diff --git a/docs-site/src/content/docs/api/createComponent.md b/docs-site/src/content/docs/api/createComponent.md index b47d610..b5ef4b0 100644 --- a/docs-site/src/content/docs/api/createComponent.md +++ b/docs-site/src/content/docs/api/createComponent.md @@ -22,7 +22,7 @@ To create a Regor component, call the `createComponent` function with the follow - `options` (optional): An array of strings that defines component properties or an object that allows you to configure various options for the component, such as whether to use interpolation, props, the component's default name, or the component context. - `context` (optional): A function that defines the Regor context for the component. This function receives a `ComponentHead` object, which you can use to specify the component's behavior and props. It should return the Regor context. - - `head.autoProps`: Automatically assigns properties defined in the `:props` binding to the component context. Defaults to `true`. + - `head.autoProps`: Automatically assigns properties defined in the `:context` binding to the component context. Defaults to `true`. - `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. @@ -94,3 +94,4 @@ The `createComponent` function returns a component object with the following pro - [`toJsonTemplate`](/api/toJsonTemplate) [Back to the API list](/api/) + diff --git a/docs-site/src/content/docs/api/index.md b/docs-site/src/content/docs/api/index.md index e3ba0f3..4864e9c 100644 --- a/docs-site/src/content/docs/api/index.md +++ b/docs-site/src/content/docs/api/index.md @@ -1,75 +1,83 @@ --- title: Regor API sidebar: - order: 5 + order: 6 --- -In this section, you will find a list of Regor's API methods organized by categories. Click on each method to view its detailed documentation. +This section contains the function-level reference for Regor. -**App / Component Template Functions:** +Use this page as an index, then open each API page for signature details and examples. -- [`createApp`](/api/createApp) -- [`createComponent`](/api/createComponent) -- [`toFragment`](/api/toFragment) -- [`toJsonTemplate`](/api/toJsonTemplate) -- [`RegorConfig`](/api/regorConfig) +## App and Components -**Cleanup Functions:** +1. [`createApp`](./createApp) +2. [`createComponent`](./createComponent) +3. [`toFragment`](./toFragment) +4. [`toJsonTemplate`](./toJsonTemplate) +5. [`RegorConfig`](./regorConfig) -- [`addUnbinder`](/api/addUnbinder) -- [`getBindData`](/api/getBindData) -- [`removeNode`](/api/removeNode) -- [`unbind`](/api/unbind) +## Reactivity -**Compute Functions:** +1. [`ref`](./ref) +2. [`sref`](./sref) +3. [`isRef`](./isRef) +4. [`isDeepRef`](./isDeepRef) +5. [`unref`](./unref) +6. [`pause`](./pause) +7. [`resume`](./resume) +8. [`trigger`](./trigger) +9. [`entangle`](./entangle) -- [`computed`](/api/computed) -- [`computeRef`](/api/computeRef) -- [`computeMany`](/api/computeMany) -- [`watchEffect`](/api/watchEffect) -- [`collectRefs`](/api/collectRefs) -- [`silence`](/api/silence) +## Computed and Effects -**Misc Functions:** +1. [`computed`](./computed) +2. [`computeRef`](./computeRef) +3. [`computeMany`](./computeMany) +4. [`watchEffect`](./watchEffect) +5. [`collectRefs`](./collectRefs) +6. [`silence`](./silence) -- [`flatten`](/api/flatten) -- [`isRaw`](/api/isRaw) -- [`markRaw`](/api/markRaw) -- [`persist`](/api/persist) -- [`html`](/api/html) -- [`raw`](/api/raw) +## Observation and Batch -**Observe Functions:** +1. [`observe`](./observe) +2. [`observeMany`](./observeMany) +3. [`observerCount`](./observerCount) +4. [`batch`](./batch) +5. [`startBatch`](./startBatch) +6. [`endBatch`](./endBatch) -- [`observe`](/api/observe) -- [`observeMany`](/api/observeMany) -- [`observerCount`](/api/observerCount) -- [`batch`](/api/batch) -- [`startBatch`](/api/startBatch) -- [`endBatch`](/api/endBatch) +## Lifecycle and Scope -**Reactivity Functions:** +1. [`useScope`](./useScope) +2. [`onMounted`](./onMounted) +3. [`onUnmounted`](./onUnmounted) -- [`ref`](/api/ref) -- [`sref`](/api/sref) -- [`isDeepRef`](/api/isDeepRef) -- [`isRef`](/api/isRef) -- [`pause`](/api/pause) -- [`resume`](/api/resume) -- [`trigger`](/api/trigger) -- [`unref`](/api/unref) -- [`entangle`](/api/entangle) +## Cleanup and Unbind -**Composition Functions:** +1. [`addUnbinder`](./addUnbinder) +2. [`getBindData`](./getBindData) +3. [`removeNode`](./removeNode) +4. [`unbind`](./unbind) -- [`useScope`](/api/useScope) -- [`onMounted`](/api/onMounted) -- [`onUnmounted`](/api/onUnmounted) +## Utilities -**Log Configuration:** +1. [`flatten`](./flatten) +2. [`markRaw`](./markRaw) +3. [`isRaw`](./isRaw) +4. [`persist`](./persist) +5. [`html`](./html) +6. [`raw`](./raw) -- [`warningHandler`](/api/warningHandler) +## Logging -Each link will take you to a dedicated documentation page for the respective method, where you can find detailed information, examples, and usage guidelines. +1. [`warningHandler`](./warningHandler) -[Back to the main](/) +## Recommendation + +Start with: + +1. [`createApp`](./createApp) +2. [`ref`](./ref) +3. [`sref`](./sref) +4. [`computed`](./computed) +5. [`watchEffect`](./watchEffect) diff --git a/docs-site/src/content/docs/directives/context.md b/docs-site/src/content/docs/directives/context.md new file mode 100644 index 0000000..246bf6b --- /dev/null +++ b/docs-site/src/content/docs/directives/context.md @@ -0,0 +1,54 @@ +--- +title: :context Directive +--- + +`:context` assigns an object into a component instance context, reactively. +`r-context` is an alias with the same behavior. + +Use it when you want to pass multiple values to a component in one place, including fields that are not declared in `props`. + +## What it does + +```html + + +``` + +In the component, `title` and `theme` become available as component context fields (or update existing ref fields when applicable). +When `pageTitle` or `activeTheme` changes, the component values are updated. + +## When to use `:context` + +Use `:context` for object-style component input. +`r-context` is equivalent and can be used interchangeably. + +Use `:prop-name="value"` or `r-bind:prop-name="value"` (same single-prop binding behavior) when the prop is declared in the component `props` list. + +Use object-form attribute binding (`r-bind="{...}"` or `:="{...}"`) for normal host attribute fallthrough, not for component context assignment. + +## Example + +```html + +``` + +```ts +const UserCard = createComponent('
...
', { + props: ['userId'], + context: (head) => ({ + userId: head.props.userId, + badge: head.props.badge, + canEdit: head.props.canEdit, + }), +}) +``` + +In this example: + +- `userId` comes from declared prop binding (`:user-id`) +- `badge` and `canEdit` come from `:context` + +[Back to the directives](/directives/) diff --git a/docs-site/src/content/docs/directives/index.md b/docs-site/src/content/docs/directives/index.md index e458222..f1d3b34 100644 --- a/docs-site/src/content/docs/directives/index.md +++ b/docs-site/src/content/docs/directives/index.md @@ -1,50 +1,56 @@ --- title: Directives sidebar: - order: 4 + order: 5 --- -In Regor, directives are special attributes that start with the "r-" prefix. They allow you to enhance the behavior and appearance of your applications. Below, you'll find a list of supported directives categorized by their functionality. +Directives are Regor’s runtime template control surface. -> **Note:** The directive prefix "r-" can be customized using `RegorConfig.getDefault().setDirectives('v-')` to align with a different naming convention, such as Vue's "v-" prefix. +By default, directive names start with `r-`. You can customize the prefix with `RegorConfig`. -**Binding Directives:** +## Core Rules -- [`r-bind`](/directives/r-bind) Binds an element's attribute to a component's data, allowing dynamic updates. -- [`r-model`](/directives/r-model) Enables two-way data binding between form inputs. -- [`r-text`](/directives/r-text) Sets the element's text content to the result of an expression. -- [`r-html`](/directives/r-html) Renders the result of an expression as HTML content within the element. -- [`r-on`](/directives/r-on) Attaches event listeners to the element and invokes specified component methods. -- [`r-show`](/directives/r-show) Conditionally displays the element based on the truthiness of an expression. +1. `r-` directives are explicit full-form syntax. +2. `:` is shorthand for `r-bind`. +3. `@` is shorthand for `r-on`. +4. `.` shorthand binds DOM properties (equivalent to `r-bind.prop`). -**Control Flow Directives:** +## Binding -- [`r-for`](/directives/r-for) Renders a set of elements based on an array and a template. -- [`r-if`](/directives/r-if) Conditionally renders the element based on the truthiness of an expression. -- [`r-else`](/directives/r-if) Provides an alternative rendering when used in conjunction with `r-if`. -- [`r-else-if`](/directives/r-if) Conditionally renders the element as an alternative to `r-if`. +1. [`r-text`](./r-text): text content binding. +2. [`r-html`](./r-html): HTML content binding. +3. [`r-bind`](./r-bind): attributes/properties/object binding. +4. [`r-model`](./r-model): form two-way binding. +5. [`r-on`](./r-on): event binding. +6. [`r-show`](./r-show): conditional visibility. +7. [`:class`](./class): dynamic class binding. +8. [`:style`](./style): dynamic style binding. -**Utility Directives:** +## Control Flow -- [`r-pre`](/directives/r-pre) Excludes HTML element from Regor bindings. -- [`:class`](/directives/class) Binds one or more class names to an element based on expressions. -- [`:style`](/directives/style) Binds one or more inline styles to an element based on expressions. -- [`:ref`](/directives/ref) Provides a reference to an element in the template, allowing you to interact with it programmatically. -- [`:key`](/directives/r-for) Provides a unique identifier for each item in a list, aiding efficient updates and rendering. -- [`:is`](/directives/is) Specifies the component to dynamically render based on a value or expression. -- [`r-teleport`](/directives/r-teleport): Teleports the element to anywhere in the DOM. Unlike Vue, teleport is a directive to avoid component overhead. -- [`:props`](/directives/props) Vue uses `v-bind` for component property passing. However, this can conflict with `v-bind`'s attribute fall-through logic. Hence, Regor defines a dedicated directive to pass properties using object syntax. It enables passing properties without defining them in the component's props contract. -- [`:props-once`](/directives/props-once) Similar to `:props` but it doesn't observe the entire reactive tree of the template expression. Tail reactivity still works. +1. [`r-if`](./r-if), `r-else-if`, `r-else`: conditional branches. +2. [`r-for`](./r-for): list/object iteration with keying support. -**Event Handling Directives:** +## Component and Utility -- [`@`](/directives/r-on) Shorthand for `r-on` to bind event listeners. +1. [`:is`](./is): dynamic component selection. +2. [`:context`](./context): component prop object binding. +3. [`:ref`](./ref): element/component references. +4. [`r-pre`](./r-pre): skip compilation for subtree. +5. [`r-teleport`](./r-teleport): move DOM subtree to another target. -**Attribute Binding Directives:** +## Important Keying Note -- [`:`](/directives/r-bind) Shorthand for `r-bind` to bind element attributes. -- [`.`](/directives/r-bind) Shorthand for `r-bind.prop` to set properties. +In `r-for`, both `key="row.id"` and `:key="row.id"` are expression bindings in Regor templates. Both are supported. Nested paths like `a.b.c.d` are supported. -These directives empower you to create dynamic and interactive user interfaces, enhancing the user experience of your Regor-powered applications. Click on each directive to view its detailed documentation, including examples and usage guidelines. +Use stable keys whenever list identity matters. -[Back to the main](/) +## Recommendation + +Read in this order: + +1. [`r-bind`](./r-bind) +2. [`r-text`](./r-text) +3. [`r-if`](./r-if) +4. [`r-for`](./r-for) +5. [`r-model`](./r-model) diff --git a/docs-site/src/content/docs/directives/props-once.md b/docs-site/src/content/docs/directives/props-once.md deleted file mode 100644 index 773f193..0000000 --- a/docs-site/src/content/docs/directives/props-once.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: :props-once Directive ---- - -The `:props-once` directive in Regor is used to bind expressions to a component's property, allowing dynamic updates. It is similar to the `r-bind` directive but specifically focuses on passing properties to components. - -Here's a key distinction: While `:props` observes every term in the expression and reacts to changes anywhere along the expression chain, `:props-once` focuses solely on the tail term, making it reactive only to changes in that part of the expression. - -## Usage - -The `:props-once` directive has the same syntax as the `r-bind` directive, allowing you to define component properties. - -```html - -``` - -- `MyComponent`: The registered component name. -- `title`: The name of the property you want to bind. - -## Notes - -- The `:props-once` directive enables binding properties not defined in component's props definition. - -[Back to the directives](/directives/) diff --git a/docs-site/src/content/docs/directives/props.md b/docs-site/src/content/docs/directives/props.md deleted file mode 100644 index c59d7ac..0000000 --- a/docs-site/src/content/docs/directives/props.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: :props Directive ---- - -The `:props` directive in Regor is used to bind expressions to a component's property, allowing dynamic updates. It is similar to the `r-bind` directive but specifically focuses on passing properties to components. - -## Usage - -The `:props` directive has the same syntax as the `r-bind` directive, allowing you to define component properties. - -```html - -``` - -- `MyComponent`: The registered component name. -- `title`: The name of the property you want to bind. - -## Notes - -- The `:props` directive enables binding properties not defined in component's props definition. - -[Back to the directives](/directives/) diff --git a/docs-site/src/content/docs/directives/r-bind.md b/docs-site/src/content/docs/directives/r-bind.md index 0614a85..ca68549 100644 --- a/docs-site/src/content/docs/directives/r-bind.md +++ b/docs-site/src/content/docs/directives/r-bind.md @@ -2,109 +2,79 @@ title: r-bind Directive --- -The `r-bind` directive in Regor is used to bind an element's attribute to a component's data, allowing dynamic updates. It is similar to Vue's `v-bind` directive and is a fundamental part of creating dynamic and interactive user interfaces. +`r-bind` is Regor’s generic binding directive. -## Usage +It maps to attribute binding by default, with property binding available via shorthand/flag. -The `r-bind` directive is typically used in the following format: +## Syntax ```html - + + + + ``` -- `element`: The HTML element to which the attribute is bound. -- `attribute`: The name of the attribute you want to bind. -- `expression`: An expression or component data property that will be evaluated and used to update the attribute's value. +1. `:` is shorthand for `r-bind:` and uses attribute binding. +2. `.` shorthand uses property binding. +3. `.prop` flag switches `:` binding to property binding. +4. `.camel` camelizes option key before binding. -### Shorthand +## Exact runtime mapping -You can use the shorthand notation `:` to achieve the same effect as `r-bind`. For example, the following two lines of code are equivalent: +From `RegorConfig`: -```html - - -``` - -### Supported Flags +1. `r-bind:*` and `:*` use `attrDirective`. +2. `.*` and `:*.prop` use `propDirective`. +3. `:class` / `r-bind:class` are routed to `classDirective`. +4. `:style` / `r-bind:style` are routed to `styleDirective`. -The `r-bind` directive supports flags that allow you to control attribute binding behavior: +## Supported value shapes -- `.prop`: Use the `.prop` flag to bind to an element's property instead of its attribute. This is useful when dealing with properties that have complex behavior, such as input values. +When an option is present (`:title="x"`), the first resolved value is used. - ```html - - ``` +When binding without an option (`r-bind="expr"`), these forms are supported: -- `.camel`: The .camel flag allows you to use kebap-case attribute names in your expressions when actual binding occurs in a camel case property, making it stick to the HTML standards. +1. Object: `{ key: value, ... }` +2. Pair array: `['key', value]` (and arrays of pairs) +3. Flat key/value sequence: `key, value, key2, value2` -### Object Syntax - -You can use object syntax with the `r-bind` directive to bind multiple attributes at once. This is particularly useful when binding multiple attributes to the same expression or when using dynamic attribute names. +Example: ```html - +
+
``` -For example: +## Dynamic key binding -```html - -``` - -In the above example, multiple attributes of the `img` element are bound to corresponding component data properties using object syntax. - -## Examples - -### Binding an Element's `src` Attribute - -In this example, we use the `r-bind` directive to bind the `src` attribute of an `img` element to a component data property `imageUrl`. When the `imageUrl` data property changes, the `src` attribute of the `img` element is updated dynamically. +For template-authored dynamic option keys, Regor uses normalized dynamic markers: ```html -Image + ``` -### Dynamic Styling - -You can use the `r-bind` directive to dynamically set the `style` attribute of an element based on component data. For example: - -```html -
-``` - -In the above example, `dynamicStyles` can be a computed property or method that returns an object with CSS style properties. +When dynamic **attribute** key changes, previous attribute is removed and new attribute is set. ## Notes -- The `r-bind` directive is a powerful tool for creating dynamic and reactive user interfaces in Regor. - -- You can use it to bind various attributes of HTML elements, including `src`, `href`, `style`, and more. +1. Attribute path handles boolean attributes and `xlink:*` attributes. +2. For DOM properties (`value`, `checked`, `innerHTML`, etc.), use `.` shorthand or `.prop`. +3. For component prop-object assignment, use [`:context`](./context), not `r-bind="{...}"`. -- Expressions in the `r-bind` directive are evaluated in the context of the component, allowing you to access and update component data properties. +## Example -- Changes to the bound data properties will automatically trigger updates to the associated element attributes. - -- You can use `r-bind` in combination with other directives like `r-model`, `:class`, and `:style` to create complex and interactive UIs. - -- Keep in mind that the `r-bind` directive is not limited to standard HTML attributes; you can also use it to bind to custom attributes in your components. +```html +Profile + +
+``` ## See Also -- [r-model Directive](/directives/r-model) -- [r-text Directive](/directives/r-text) -- [r-html Directive](/directives/r-html) -- [r-on Directive](/directives/r-on) -- [r-show Directive](/directives/r-show) +1. [r-text](./r-text) +2. [r-model](./r-model) +3. [r-on](./r-on) +4. [r-for](./r-for) + -[Back to the directives](/directives/) diff --git a/docs-site/src/content/docs/directives/r-for.md b/docs-site/src/content/docs/directives/r-for.md index 1c500d1..84d1096 100644 --- a/docs-site/src/content/docs/directives/r-for.md +++ b/docs-site/src/content/docs/directives/r-for.md @@ -2,163 +2,75 @@ title: r-for Directive --- -The `r-for` directive is a powerful tool in Regor that allows you to iterate over an array or iterable and generate repetitive elements based on a template. It enables you to efficiently render lists of items and is especially useful for creating dynamic content in your templates. +`r-for` renders repeated template instances from arrays, iterables, and object-like values. ## Usage -To use the `r-for` directive, apply it to an element in your template, followed by an expression that represents the data you want to iterate over. The directive creates a template that is duplicated for each item in the iterable, providing a way to render dynamic content. - ```html - - - - {{ item }} - +
  • {{ item }}
  • ``` -- `item`: Represents the current item in the iteration. -- `items`: An array or iterable from which items are taken for rendering. - -### Example +You can use `in` or `of`: ```html - +
  • {{ item }}
  • ``` -In this example, the `r-for` directive iterates over the `fruits` array and generates an `
  • ` element for each fruit, displaying the fruit's name. - ## Index Variable -You can also access the index of the current iteration using the `#` character in front of any variable, which is commonly used when you need to track the position of each item in the iterable. - -`#` prefix enables accessing to individual indexes in nested `r-for` loops. +Use `#`-prefixed variable for index: ```html - - - - {{ index + 1 }}. {{ item }} - +
  • {{ i }} - {{ item }}
  • ``` -- `item`: Represents the current item in the iteration. -- `#index`: Represents the index of the current item (zero-based). -- `items`: An array or iterable from which items are taken for rendering. - -### Example - -```html - -``` - -In this example, the `r-for` directive generates a numbered list of fruits, displaying both the index and the fruit name. - ## Iterating Over Objects -You can use the `r-for` directive to iterate over the properties of an object. - -```html - - - - {{ key }}: {{ value }} - -``` - -- `key`: Represents the current property's key (name). -- `value`: Represents the current property's value. -- `object`: An object whose properties are iterated over. - -### Example +Object iteration is supported: ```html - +
  • {{ key }}: {{ value }}
  • ``` -In this example, the `r-for` directive iterates over the properties of the `person` object and displays each property's key-value pair. - ## Object Destructuring -You can use object destructuring to extract specific properties from the items in the iterable when using the r-for directive. This allows you to access and display only the properties you need within the repeated content. - -```html - - - - {{ property1 }} - {{ property2 }} (Index: {{ index }}) - -``` - -- `{ property1, property2 }`: Specifies the properties to be extracted from each item in the iterable. -- `#index`: Represents the index of the current item (zero-based). -- `items`: An array or iterable from which items are taken for rendering. - -### Example - -```html - -``` - -In this example, the `r-for` directive iterates over the `users` array and extracts the `name` and `age` properties from each user object. It then displays both the `name`, `age`, and the `index` of each user. - -Object destructuring provides fine-grained control over which properties are utilized within the repeated content, enhancing flexibility and readability in your templates. - -## Iterating with `of` and `in` - -You can use either `of` or `in` to specify the iteration variable and the iterable, depending on your preference. Both are equivalent in functionality. +Destructuring is supported: ```html - - - - {{ fruit }} - +
  • + {{ i }} - {{ name }} ({{ age }}) +
  • ``` ## Complex Expressions -You can pass any JavaScript expression after `in` or `of`. This allows using -helpers like `filter` directly within the directive. +Any valid expression can provide iterable data: ```html ``` -## Key Attribute +## Keying (`key` and `:key`) -When iterating over lists of items, it's important to provide a unique `key` attribute to help Regor efficiently track and update elements. The `key` attribute should be applied to the repeated element, and its value should be unique for each item in the iterable. +For stable identity and predictable updates, set a key on repeated nodes. ```html - +
  • ``` -In this example, the `:key` attribute is set to `id`, ensuring that each `
  • ` element has a unique identifier based on user's id. +Important behavior in Regor templates: -## Table Templates +1. `key="row.id"` and `:key="row.id"` are both expression-based for list keying. +2. Nested key paths are supported, including `a.b.c.d`. +3. Use stable, unique keys from your domain model. -`r-for` is supported in table templates, including component-based rows and -cells. +If keys are unstable, DOM reuse quality degrades and updates become less predictable. -- Supported containers: `table`, `thead`, `tbody`, `tfoot`. -- Component rows can be rendered from these containers. -- Component-like direct children of `` are normalized to cell hosts to keep - valid table structure. +## Table Templates -Example: +`r-for` supports native table structures and component-based rows/cells. ```html @@ -182,14 +94,15 @@ const tableRow = createComponent( ) ``` -Regor keeps table templates valid across `table`, `thead`, `tbody`, and `tfoot` -while supporting component-based rows and cells. - -## Notes +## Best Practices -- The `r-for` directive is a powerful tool for rendering dynamic lists and iterables. -- You can use `#` prefix to access the current iteration's index. +1. Keep row templates minimal. +2. Use stable keying. +3. Avoid expensive per-row expressions in large lists. +4. Prefer pagination/windowing for very large data sets. -The `r-for` directive in Regor simplifies the process of rendering dynamic lists of items, making your templates more flexible and data-driven. +## See Also -[Back to the directives](/directives/) +1. [r-if](./r-if) +2. [r-bind](./r-bind) +3. [r-text](./r-text) diff --git a/docs-site/src/content/docs/directives/r-text.md b/docs-site/src/content/docs/directives/r-text.md index a94a839..7f89174 100644 --- a/docs-site/src/content/docs/directives/r-text.md +++ b/docs-site/src/content/docs/directives/r-text.md @@ -2,51 +2,57 @@ title: r-text Directive --- -The `r-text` directive is used to set the text content of an HTML element to the result of an expression. It allows you to dynamically update the text displayed within an element based on the evaluated value of the provided expression. +`r-text` binds an expression to an element’s `textContent`. + +It is the safest and most direct way to render text output. ## Syntax ```html - +

    ``` -- ``: The HTML element to which you want to bind the text content. -- `expression`: An expression that determines the text content to be displayed within the element. +## Basic Example -## Usage +```html +

    +``` -The `r-text` directive is particularly useful when you want to insert dynamic data or computed values into your HTML content. +When `message` changes, the element text updates reactively. -### Example +## Expression Example ```html -
    -

    -
    +

    ``` -In this example, the text content of the `

    ` element will be dynamically updated based on the value of the `message` property in your component's data. Any changes to the `message` property will automatically reflect in the displayed text. - -## Notes +## `r-text` vs `{{ ... }}` -- The `r-text` directive updates the text content of an element and does not interpret HTML tags. Use `r-html` if you need to render HTML content. +Both are valid. Prefer `r-text` when: -- You can use expressions and variables in the `expression` attribute to display dynamic content. For example, you can concatenate strings, perform calculations, or reference component properties. +1. You want explicit binding at element level. +2. You are optimizing repeated row templates and want predictable binding points. +3. You want to avoid accidental surrounding whitespace behavior from text nodes. -- The `r-text` directive is reactive, meaning that if the value of the `expression` changes, the displayed text will update accordingly in real-time. +## Security and Behavior -- You can combine the `r-text` directive with other directives and HTML attributes to create complex dynamic templates. +1. `r-text` writes plain text, not HTML. +2. HTML tags in values are escaped as text. +3. Use [`r-html`](./r-html) only when you intentionally need HTML rendering. -## Example +## Common Patterns ```html -

    -

    -
    +
  • + + +
  • ``` -In this example, if the `greeting` property in your component's data is initially set to "Hello, Regor!", the text displayed within the `

    ` element will be "Hello, Regor!". If you later update the `greeting` property to "Welcome to Regor!", the displayed text will automatically change to "Welcome to Regor!". +This is a clear and performant default for list text output. -The `r-text` directive simplifies the process of updating text content within your templates, allowing you to create dynamic and responsive user interfaces with ease. +## See Also -[Back to the directives](/directives/) +1. [r-bind](./r-bind) +2. [r-html](./r-html) +3. [r-for](./r-for) diff --git a/docs-site/src/content/docs/directives/style.md b/docs-site/src/content/docs/directives/style.md index 6381714..9767acd 100644 --- a/docs-site/src/content/docs/directives/style.md +++ b/docs-site/src/content/docs/directives/style.md @@ -4,6 +4,8 @@ title: :style Directive The `:style` directive in Regor allows you to dynamically apply inline styles to HTML elements. It provides a way to bind and update the `style` attribute of an element based on data or expressions. With `:style`, you can create dynamic and responsive styling for your components. +`r-style` is an alias of `:style` and behaves the same way. + ## Syntax ```html diff --git a/docs-site/src/content/docs/getting-started.md b/docs-site/src/content/docs/getting-started.md index a4acbee..3174702 100644 --- a/docs-site/src/content/docs/getting-started.md +++ b/docs-site/src/content/docs/getting-started.md @@ -1,56 +1,166 @@ --- title: Getting Started sidebar: - order: 2 + order: 3 --- -Welcome to Regor! This guide will walk you through the essential steps to start building HTML5-based applications using the Regor UI framework. Whether you're new to Regor or an experienced developer, this guide will help you get up and running quickly. +Regor binds runtime behavior directly to DOM you already have, or to template +content you provide at mount time. -## Installation - -Before you can begin using Regor, you need to add it to your project. You can install Regor using npm or yarn. - -**Using yarn** +## Install ```sh yarn add regor ``` -**Using npm** - ```sh npm install regor ``` -## Import into Typescript file +## Quick Start (TypeScript) ```ts -import { createApp, ref } from 'regor' +import { createApp, html, ref } from 'regor' -createApp({ - message: ref('Hello world!'), -}) +const app = createApp( + { + count: ref(0), + inc() { + this.count(this.count() + 1) + }, + }, + { + element: document.querySelector('#app')!, + template: html` +

    + +

    Count:

    +
    + `, + }, +) + +// optional teardown +// app.unbind() +// app.unmount() ``` -## Using the global build +## CDN ESM ```html - +
    + +``` + +## Core Mount Modes + +1. Bind existing markup in place: + +```ts +createApp( + { userName: ref('Ada') }, + { selector: '#profile-island' }, +) +``` + +2. Replace root content from template string: + +```ts +createApp( + { title: ref('Home') }, + { + selector: '#app', + template: '

    ', + }, +) ``` -## Using the module script +3. Replace root content from JSON template: + +```ts +createApp( + { label: ref('Save') }, + { + element: document.querySelector('#app')!, + json: { t: 'button', a: { 'r-text': 'label' } }, + }, +) +``` + +## Reactivity Basics + +`ref` for values and deep object conversion: + +```ts +import { ref } from 'regor' + +const count = ref(0) +count(1) +console.log(count()) +``` + +`sref` for shallow object state: + +```ts +import { sref } from 'regor' + +const user = sref({ name: 'Ada', age: 30 }) +user().name = 'Grace' +user({ ...user(), age: 31 }) +``` + +Inside templates, refs are auto-unwrapped, so use `count`, `user.name`, not +`count()` or `.value`. + +## Component Quick Example + +```ts +import { createApp, createComponent, ref } from 'regor' + +const UserCard = createComponent('

    ', { + props: ['name'], + context: (head) => ({ + name: head.props.name ?? 'Anonymous', + }), +}) + +createApp({ + components: { UserCard }, + activeName: ref('Ada Lovelace'), +}) +``` ```html -
    {{ message }}
    - + ``` -[Back to the main](/) +## Template Syntax You Will Use Daily + +1. `r-text="expr"` for text. +2. `:attr="expr"` or `r-bind:attr="expr"` for attributes. +3. `.prop="expr"` or `:prop.prop="expr"` for DOM properties. +4. `@event="handler"` for events. +5. `r-if` / `r-else-if` / `r-else` for conditional branches. +6. `r-for="item, #i in items"` for loops. +7. `:context="{ ... }"` / `r-context="{ ... }"` for object-style component input. +8. `{{ expr }}` and `[[ expr ]]` interpolation (enabled by default). + +## Where To Go Next + +1. [Overview](./overview) +2. [Guide](./guide) +3. [Directives](./directives) +4. [API Reference](./api) +5. [Guide: Mounting](./guide/mounting) diff --git a/docs-site/src/content/docs/guide/components.md b/docs-site/src/content/docs/guide/components.md new file mode 100644 index 0000000..524751e --- /dev/null +++ b/docs-site/src/content/docs/guide/components.md @@ -0,0 +1,207 @@ +--- +title: Components +sidebar: + order: 30 +--- + +This guide documents how Regor components work in runtime, based on current implementation and tests. + +## Create a Component + +```ts +import { createComponent, html } from 'regor' + +export const UserCard = createComponent( + html`

    `, + { props: ['title'] }, +) +``` + +`createComponent(template, options)` supports: + +1. Template from string: `createComponent('
    ...
    ')` +2. Template object with `template` +3. Template object with `element` +4. Template object with `selector` +5. Template object with `json` + +Interpolation is enabled by default (`useInterpolation: true`). + +## Register and Use Components + +Register via app context: + +```ts +createApp({ + components: { UserCard }, +}) +``` + +or via `RegorConfig.addComponent(...)` (registered/global style). + +Tags resolve in component map using component name and kebab-case variant. + +```html + +``` + +## Component Context and `ComponentHead` + +Component context is created by `options.context(head)`. +`head` is `ComponentHead` and exposes: + +1. `head.props`: resolved incoming props object. +2. `head.emit(event, detail)`: dispatches `CustomEvent` from host element. +3. `head.autoProps` (default `true`): auto-assigns props into component context fields. +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. + +### Emit Example + +```ts +class CardContext { + title = ref('local') + $emit?: (event: string, args: Record) => void + save = () => this.$emit?.('save', { title: this.title() }) +} + +const Card = createComponent('', { + context: () => new CardContext(), +}) +``` + +In parent: + +```html + +``` + +## How Component Inputs Are Routed + +On a component host tag, Regor routes bindings through two different channels: + +1. **Component input channel**: values end up in `head.props` and can become component state. +2. **Attribute fallthrough channel**: values are copied as DOM attributes/class/style to rendered output root. + +### 1) Declared single-prop bindings (component input channel) + +Use: + +1. `:x="..."` +2. `.x="..."` +3. `r-bind:x="..."` + +These are treated as component props **only if `x` is declared in `props: [...]`**. + +```ts +const Card = createComponent('

    ', { + props: ['title'], + context: (head) => ({ title: head.props.title }), +}) +``` + +```html + +``` + +If `x` is not declared in `props`, that single binding is treated as normal attribute fallthrough. + +### 2) Object component input (`:context` / `r-context`) + +Use: + +```html + + +``` + +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 + +```html + +``` + +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 + +1. `head.autoProps = false`: component author maps from `head.props` manually. +2. `head.autoProps = true` + `head.entangle = true`: ref-to-ref inputs are two-way entangled. +3. `head.autoProps = true` + `head.entangle = false`: ref-to-ref inputs are initial snapshot only. +4. Primitive/object values targeting existing component ref fields are applied to those refs. + +## Slots + +Supported slot patterns: + +1. Default slot: `` +2. Named slot: `` +3. Named slot shorthand in component template: `` +4. Host-side named template: `` or `` +5. Fallback slot content when host does not provide matching content. + +Default slot behavior: + +1. If host provides unnamed content, it is used. +2. Named-only template shortcuts are not injected into default slot. + +For parent-context slot expression switching, enable: + +```ts +context: (head) => { + head.enableSwitch = true + return {} +} +``` + +## Attribute inheritance (`inheritAttrs`) + +By default, component host attributes are inherited into component output root (`inheritAttrs: true`). + +Details: + +1. `class` is merged. +2. `style` properties are merged. +3. Other attributes are copied. +4. `:context` is excluded from inherit copy. +5. If component has multiple root elements, `r-inherit` can mark intended inheritor. + +Set `inheritAttrs: false` to disable this behavior. + +## Dynamic components with `:is` + +```html +
    +``` + +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 + +1. Declare single props in `props` when using individual bindings. +2. Use `:context` for object-style prop passing. +3. Use `head.enableSwitch = true` when slot content must evaluate in parent context. +4. Decide `autoProps` + `entangle` explicitly when designing parent-child data ownership. +5. Keep fallback slot content for robust component defaults. +6. Use stable keys for component lists rendered with `r-for`. + +## See Also + +1. [createComponent API](../api/createComponent) +2. [Directive: :is](../directives/is) +3. [Directive: :context](../directives/context) +4. [TypeScript Guide](./typescript) +5. [Directive: r-for](../directives/r-for) diff --git a/docs-site/src/content/docs/guide/index.md b/docs-site/src/content/docs/guide/index.md new file mode 100644 index 0000000..cc5f462 --- /dev/null +++ b/docs-site/src/content/docs/guide/index.md @@ -0,0 +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](./reactivity) + Core primitives (`ref`, `sref`, computed, observers), update semantics, and state design. + +2. [Templates and Expressions](./templates) + Interpolation transform, parser/evaluator capabilities, and template authoring patterns. + +3. [Components](./components) + Component creation, input channels (`:x`, `r-bind:x`, `:context`, `r-context`), slots, and inherit behavior. + +4. [TypeScript](./typescript) + Strong typing patterns for app contexts, class-based contexts, and `ComponentHead`. + +5. [Lifecycle and Cleanup](./lifecycle-and-cleanup) + Mounted/unmounted hooks, `unmount` vs `unbind`, and `drainUnbind` usage. + +6. [Mounting](./mounting) + Static-first + dynamic islands and existing-markup binding patterns. + +7. [Persisted State](./persisted-state) + `persist(...)` patterns for durable reactive state. + +8. [Performance](./performance) + Benchmark methodology, bottleneck analysis, and optimization workflow. + +9. [Troubleshooting](./troubleshooting) + Symptom -> cause -> fix guide for common runtime issues. + diff --git a/docs-site/src/content/docs/guide/lifecycle-and-cleanup.md b/docs-site/src/content/docs/guide/lifecycle-and-cleanup.md new file mode 100644 index 0000000..940f4eb --- /dev/null +++ b/docs-site/src/content/docs/guide/lifecycle-and-cleanup.md @@ -0,0 +1,122 @@ +--- +title: Lifecycle and Cleanup +sidebar: + order: 50 +--- + +This page describes Regor lifecycle and teardown behavior from the actual runtime flow. + +## Runtime lifecycle order + +`createApp(...)` runs in this order: + +1. Resolve root (`element`, `selector`, or default `#app` for string template shortcut). +2. If `template`/`json` is provided, replace root content first. +3. Run interpolation (when enabled in config). +4. Bind directives/components. +5. Register root cleanup callback. +6. Call mounted lifecycle (`onMounted(...)` callbacks and optional `context.mounted()` method). + +## Hook registration with `useScope` + +`onMounted` / `onUnmounted` are scope-based APIs. +In practice, register them while creating context inside `useScope(...)`: + +```ts +import { createApp, html, onMounted, onUnmounted, ref, useScope } from 'regor' + +class AppCtx { + count = ref(0) + logs: string[] = [] + + constructor() { + onMounted(() => this.logs.push('mounted')) + onUnmounted(() => this.logs.push('unmounted')) + } +} + +const app = createApp( + useScope(() => new AppCtx()), + { + element: document.querySelector('#app')!, + template: html`

    `, + }, +) +``` + +Calling these hooks outside an active scope throws (`ComposablesRequireScope`). + +## `unmount()` vs `unbind()` + +Returned app exposes both: + +1. `app.unmount()` + - Removes root node from DOM (`removeNode(root)`). + - Unbind runs through deferred queue. +2. `app.unbind()` + - Unbinds root subtree immediately. + - Keeps DOM in place. + +Use `unmount()` when app is gone. +Use `unbind()` when markup stays but Regor behavior must stop. + +## Deferred cleanup queue and `drainUnbind()` + +`removeNode(...)` queues unbind work and flushes it with a short timer. +This keeps removal fast, but cleanup side effects are not always immediate in the same tick. + +`drainUnbind()` forces pending queue flush now. + +Typical test teardown: + +```ts +import { createApp, drainUnbind, html, ref } from 'regor' + +const root = document.createElement('div') +const app = createApp( + { n: ref(1) }, + { element: root, template: html`

    ` }, +) + +app.unmount() +await drainUnbind() +``` + +## Component lifecycle notes + +Component teardown is registered through unbinders on component markers/host nodes. +When component subtree is unbound: + +1. `onUnmounted(...)` callbacks run for component context. +2. `context.unmounted?.()` runs if defined. +3. Directive observers/listeners are detached. +4. Slot-switch helper contexts are released. + +## `ComponentHead.unmount()` + +Inside component context, `head.unmount()` is a force-remove tool: + +1. Removes nodes between component start/end markers. +2. Calls unmounted lifecycle for captured component contexts. + +Use only when component wants to self-remove its rendered block. + +## Practical cleanup patterns + +1. Page-level app: + Prefer `app.unmount()` on route/page disposal. +2. Keep DOM, disable behavior: + Use `app.unbind()`. +3. Test suites: + `app.unmount()` + `await drainUnbind()` in `finally`. +4. Manual reactive resources: + If you create observers/effects outside scoped/component lifecycle, stop them explicitly. + +## See Also + +1. [API: createApp](../api/createApp) +2. [API: useScope](../api/useScope) +3. [API: onMounted](../api/onMounted) +4. [API: onUnmounted](../api/onUnmounted) +5. [API: removeNode / drainUnbind](../api/removeNode) +6. [API: unbind](../api/unbind) diff --git a/docs-site/src/content/docs/guide/mounting.md b/docs-site/src/content/docs/guide/mounting.md new file mode 100644 index 0000000..2f0c853 --- /dev/null +++ b/docs-site/src/content/docs/guide/mounting.md @@ -0,0 +1,97 @@ +--- +title: Mounting +sidebar: + order: 60 +--- + +Regor can mount directly on existing DOM and compose with many rendering/runtime strategies. + +One common pattern is static-first pages with dynamic islands, but Regor is not limited to that model. + +1. Render most page content as static HTML (SSG/SSR/build-time output). +2. Mount Regor only where runtime behavior is needed. +3. Keep static and dynamic ownership intentionally separated. + +In many projects, this can mean mostly static content with selectively mounted dynamic regions. + +## Why this is powerful + +This architecture gives you: + +1. Fast first paint from static HTML. +2. Minimal runtime cost by mounting only where needed. +3. Better control over complexity and ownership boundaries. +4. Freedom to run multiple independent Regor apps on one page. + +## Typical dynamic islands + +Examples where selective mounting is ideal: + +1. Auth/login area in top navigation. +2. Profile badge/menu/avatar state. +3. Cart counters and checkout widgets. +4. Search/filter controls. +5. Realtime status chips, notifications, dashboards. + +## Multi-mount pattern + +You can mount separate Regor contexts into multiple existing DOM roots: + +```html +
    +
    + + +
    +
    + + +``` + +```ts +import { createApp, ref, sref } from 'regor' + +const user = sref<{ name: string; avatar: string } | null>(null) +const count = ref(0) + +createApp({ user }, { element: document.querySelector('#auth-island')! }) + +createApp({ count }, { element: document.querySelector('#notif-island')! }) +``` + +This is first-class Regor usage. + +## Runtime ownership options + +When mixing runtimes and rendering layers: + +1. Static renderer owns static regions. +2. Regor owns only mounted island roots. +3. Multiple runtimes can coexist, including on nearby/overlapping areas, but behavior is easiest to reason about when ownership boundaries are explicit. +4. Use clear root boundaries per island where possible for predictability. + +## Mount modes on existing DOM + +`createApp` supports: + +1. In-place binding of existing subtree (`{ element }` or `{ selector }` without `template`/`json`). +2. Replacing root children when `template` or `json` is provided. + +Use in-place mode whenever markup is already present and you only need reactive behavior. + +## Architectural guidance + +1. Prefer many small island roots over one giant runtime root. +2. Keep high-churn UI in isolated islands. +3. Keep long-form/static content fully static. +4. Unmount islands explicitly when their host is removed. + +## See Also + +1. [Guide: Components](./components) +2. [Guide: Lifecycle and Cleanup](./lifecycle-and-cleanup) +3. [Performance Guide](./performance) diff --git a/docs-site/src/content/docs/guide/performance.md b/docs-site/src/content/docs/guide/performance.md new file mode 100644 index 0000000..7f03649 --- /dev/null +++ b/docs-site/src/content/docs/guide/performance.md @@ -0,0 +1,79 @@ +--- +title: Performance +sidebar: + order: 80 +--- + +This guide focuses on practical, reliable performance work for Regor applications. + +## Performance Priorities + +Real-world cost is mostly driven by: + +1. Number of DOM nodes created/updated. +2. Number of active bindings per node. +3. Work performed by each binding expression. +4. List scale and mutation pattern (`r-for` + key strategy). + +Many small costs add up. Always profile the full path, not one isolated helper. + +## Benchmark Correctness Checklist + +When comparing Regor with any other framework: + +1. Same rendered output and same feature set on both sides. +2. Same timing boundaries for both sides. +3. Warmup rounds are excluded from measurements. +4. A/B alternating order is used per sample to reduce bias. +5. Median and P90 are reported, not single-run results. + +If timing boundaries differ, results are not comparable. + +## Interpreting Results + +Use this practical UX scale: + +1. Under `100ms`: typically instant. +2. `100ms` to `300ms`: noticeable, usually acceptable. +3. Over `300ms`: likely perceived as slow. + +Absolute user-visible latency matters more than synthetic rank. + +## Large List Guidance (`r-for`) + +For large datasets: + +1. Prefer windowing, chunking, or pagination. +2. Keep per-row template/binding footprint minimal. +3. Use stable keys. +4. Avoid expensive per-item object construction in template expressions. +5. Consider domain-specific rendering shortcuts in known hot paths. + +## Regor-Specific Advantage + +Regor enables runtime optimization strategies that are hard to apply in rigid pipelines: + +1. Bind to existing HTML markup in place. +2. Use directives for most UI, and direct DOM/JS for selected hotspots. +3. Keep one runtime model instead of splitting between template compiler assumptions and runtime behavior. + +## Profiling Workflow + +Use repeatable tooling: + +1. Run browser benchmark for end-user behavior (`yarn perf`). +2. Run minidom perf suite for runtime isolation (`yarn perf `). +3. Run profile capture (`yarn perf:profile`) and inspect top self-time stacks. +4. Optimize only top hotspots with clear measured deltas. + +## Optimization Order + +1. Reduce nodes. +2. Reduce bindings. +3. Simplify expressions. +4. Fix one hotspot at a time with before/after measurements. +5. Protect gains with perf baselines. + +## When to Stop + +If interaction latency is already in acceptable user-perceived range, stop micro-tuning and prioritize product work. Optimize only when the user can feel the difference. diff --git a/docs-site/src/content/docs/guide/persisted-state.md b/docs-site/src/content/docs/guide/persisted-state.md new file mode 100644 index 0000000..9e84c8f --- /dev/null +++ b/docs-site/src/content/docs/guide/persisted-state.md @@ -0,0 +1,113 @@ +--- +title: Persisted State +sidebar: + order: 70 +--- + +`persist(...)` is one of Regor’s highest-leverage features: + +1. Turn any ref into persistent state with one call. +2. It returns the same ref instance, so it can be used inline in context creation. +3. State is restored from `localStorage` on startup. + +## Why it matters + +For static-first sites with dynamic islands, `persist` lets each island keep user state without extra storage wiring. + +Typical persisted states: + +1. Auth/session display model. +2. Profile UI preferences. +3. Theme, locale, and layout options. +4. Last-opened tabs, filters, and panel state. + +## Real-world sample: header + preferences + +```ts +import { createApp, persist, ref, sref } from 'regor' + +createApp( + { + // Persisted auth/profile island state + auth: persist( + sref({ + loggedIn: false, + name: '', + avatar: '', + }), + 'app:auth', + ), + + // Persisted UI preferences + prefs: persist( + sref({ + theme: 'dark', + locale: 'en', + compact: false, + }), + 'app:prefs', + ), + + // Persisted primitive ref + notifCount: persist(ref(0), 'app:notif-count'), + }, + { + element: document.querySelector('#topbar-island')!, + }, +) +``` + +```html +
    + +
    + + +
    + + + + +
    +``` + +After reload, previous values are restored automatically. + +## Inline context creation pattern + +Because `persist` returns the same ref, this is valid and recommended: + +```ts +const context = { + theme: persist(ref('dark'), 'app:theme'), + filters: persist(sref({ status: 'open', query: '' }), 'app:filters'), +} +``` + +## Behavior details + +1. If stored JSON exists, ref is hydrated from storage. +2. If stored JSON is invalid, Regor logs warning and rewrites storage with current ref value. +3. If key is empty, `persist` throws. +4. Updates are synced via reactive observation. + +## Key strategy + +Use namespaced keys: + +1. `app:auth` +2. `app:prefs` +3. `page:search:filters` + +Avoid collisions across islands/features. + +## See Also + +1. [API: persist](../api/persist) +2. [Guide: Mounting](./mounting) +3. [Guide: Reactivity](./reactivity) diff --git a/docs-site/src/content/docs/guide/reactivity.md b/docs-site/src/content/docs/guide/reactivity.md new file mode 100644 index 0000000..826e615 --- /dev/null +++ b/docs-site/src/content/docs/guide/reactivity.md @@ -0,0 +1,152 @@ +--- +title: Reactivity +sidebar: + order: 10 +--- + +This page describes Regor reactivity based on actual runtime behavior. + +## Core model + +1. A ref is a callable value container (`r()`, `r(newValue)`) with `.value` alias. +2. `ref()` and `sref()` are both refs (`isRef(...) === true`). +3. `ref()` is deep-conversion oriented. +4. `sref()` is shallow-conversion oriented. + +## `ref` vs `sref` (important) + +### `ref(value)` + +`ref` recursively converts nested object/array properties to refs. + +```ts +const user = ref({ name: 'Ada', meta: { active: true } }) +user().name('Grace') +user().meta().active(false) +``` + +Key behavior: + +1. Object/array content is converted recursively. +2. Source objects are mutated in place during conversion. +3. Existing refs are reused. +4. `Node`, `Date`, `RegExp`, `Promise`, `Error` are not recursively converted. + +### `sref(value)` + +`sref` keeps nested properties as plain values (no recursive wrapping). + +```ts +const user = sref({ name: 'Ada', age: 30 }) +user().name = 'Grace' +user().age = 31 +``` + +Key behavior: + +1. Nested values stay plain unless they were already refs. +2. Arrays/Map/Set are made reactive via prototype adaptation. +3. `sref(sourceRef)` returns the same ref instance. + +## Access and update forms + +For both `ref` and `sref`: + +```ts +const r = ref(1) +r() // get +r(2) // set +r.value // get +r.value = 3 // set +``` + +## Derived refs: `computed`, `computeRef`, `computeMany` + +Regor computed refs are: + +1. Read-only. +2. Lazy (first evaluation happens on first read). +3. Cached until dependencies change. +4. Invalidated on source change, then recomputed on next read. + +```ts +const a = ref(1) +const b = ref(2) +const sum = computeMany([a, b], (x, y) => x + y) + +sum() // 3 (computes now) +sum() // 3 (cached) +a(5) +sum() // 7 (recomputed after invalidation) +``` + +## `watchEffect` + +`watchEffect` immediately runs and tracks refs accessed during execution. + +When any tracked ref changes: + +1. Cleanup callbacks registered through `onCleanup` are called. +2. Effect is re-subscribed using newly accessed refs. + +```ts +const count = ref(0) +const stop = watchEffect((onCleanup) => { + const snapshot = count() + onCleanup?.(() => console.log('cleanup', snapshot)) + console.log('value', snapshot) +}) +``` + +Use `silence(() => ...)` to read refs without tracking. + +## `observe` and `observeMany` + +`observe(source, cb, init?)` listens to one ref. +`observeMany([a, b], cb, init?)` listens to multiple refs and returns tuple values. + +Both return a stop function. + +## Batch and manual trigger + +`batch`/`startBatch`/`endBatch` defer observer notification until batch end. + +```ts +batch(() => { + a(1) + a(2) + b(3) +}) +``` + +Observers are triggered once per changed ref after batch completion. + +Use `trigger(ref)` to manually notify observers. + +`trigger(ref, undefined, true)` recursively triggers nested refs. + +## Pause and resume + +`pause(ref)` disables auto observer firing for that ref. +`resume(ref)` re-enables it. +Manual `trigger(ref)` still works while paused. + +## Other important helpers + +1. `unref(x)` unwraps one ref level. +2. `isRef(x)` checks any ref (`ref` or `sref`). +3. `isDeepRef(x)` checks whether value was created as deep `ref`. +4. `entangle(a, b)` creates two-way sync and initializes `b` with `a`. +5. `persist(ref, key)` syncs ref data with `localStorage`. + +## Collections and caveats + +1. `Map` and `Set` are reactive through Regor’s proxy prototypes. +2. `WeakMap` and `WeakSet` are not auto-reactive; use `trigger(...)` manually when needed. +3. For very large nested structures, pick `ref` vs `sref` intentionally based on update style and cost. + +## See Also + +1. [API Reference](../api) +2. [Lifecycle and Cleanup](./lifecycle-and-cleanup) +3. [Templates and Expressions](./templates) diff --git a/docs-site/src/content/docs/guide/templates.md b/docs-site/src/content/docs/guide/templates.md new file mode 100644 index 0000000..8d25b4b --- /dev/null +++ b/docs-site/src/content/docs/guide/templates.md @@ -0,0 +1,226 @@ +--- +title: Templates and Expressions +sidebar: + order: 20 +--- + +This page describes how Regor turns templates into live bindings, and what expression syntax is supported in runtime. + +## Template Sources + +Regor accepts templates from multiple sources in `createApp(...)` / `createComponent(...)`: + +1. HTML string (`template`) +2. Existing DOM element (`element`) +3. DOM selector (`selector`) +4. JSON template (`json`) + +String templates are preprocessed before DOM parsing (table compatibility rules below). + +## Interpolation + +Regor supports two interpolation delimiters: + +1. `{{ expr }}` +2. `[[ expr ]]` + +```html +

    {{ user.name }}

    +

    [[ user.name ]]

    +``` + +## Interpolation Transform Rules + +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 `` nodes. +4. Subtrees marked with `r-pre` are skipped. + +If `useInterpolation = false`, `{{ ... }}` and `[[ ... ]]` stay as plain text. + +## Table-Safe Preprocess for String Templates + +For string templates, Regor preprocesses markup to keep table structures valid: + +1. Outside table scope: + - `
    ` / `
    ` / `` may be rewritten to alias hosts (`trx` / `tdx` / `thx`) with `is="r-*"` markers. +2. In table-sensitive positions: + - non-native direct children may be rewritten to safe hosts with `is="regor:OriginalTag"`. +3. Self-closing custom tags under row context are normalized to explicit open/close tags. + +This avoids browser table parser dropping/reparenting custom nodes. + +## Where Expressions Are Used + +Typical expression points: + +1. Text/content: `r-text`, interpolation +2. Attributes/properties: `:x`, `.x`, `r-bind:x` +3. Events: `@click`, `r-on:*` +4. Control flow: `r-if`, `r-else-if`, `r-for` +5. Styling: `:class`, `:style` +6. Components: `:is`, `:context`, `r-context` + +## Expression Scope and Resolution + +Expressions are evaluated against active context stack: + +1. Current app/component context +2. Loop aliases (`r-for`) +3. Slot switch contexts (when `head.enableSwitch = true`) +4. Global context from `RegorConfig` + +Special identifiers: + +1. `$root`: root-most context +2. `$parent`: immediate parent context +3. `$ctx`: full context stack array +4. `this`: current context object +5. `$event`: available in event/lazy evaluation paths + +Regor auto-unrefs refs in expressions, so template code uses `user.name`, not `user.name()` / `.value`. + +## Supported Expression Syntax (Runtime) + +Backed by Regor’s jsep/regorEval pipeline, templates support: + +1. Member access, optional chaining: `a.b`, `a?.b`, `a[k]` +2. Calls/new: `fn(x)`, `new Date(0)` +3. Arithmetic/comparison/logical/bitwise operators +4. Nullish coalescing / ternary: `a ?? b`, `ok ? a : b` +5. Assignments and updates: `x = 1`, `x += 2`, `x++`, `--x` +6. Arrays/objects/spread/computed object keys +7. Arrow functions +8. Template literals and tagged templates +9. Regex literals +10. Comma/compound forms + +## `r-for` Key Notes + +Both forms are supported for expression-based keys: + +1. `key="row.id"` +2. `:key="row.id"` + +Nested paths such as `a.b.c.d` are supported. +Use stable domain IDs for predictable updates. + +## Practical Patterns + +1. Prefer `r-text` for hot repeated rows. +2. Keep template expressions lightweight; move heavy logic into methods/computed refs. +3. Use stable keys in lists. +4. Use `r-pre` to protect literal template snippets from interpolation transform. + +## Advanced Samples + +### 1) Static-first page + dynamic island + +Use existing server-rendered markup and bind only the dynamic area. + +```html +
    +

    My Shop

    +
    + + Welcome, {{ userName }} +
    +
    +``` + +```ts +createApp( + { + loggedIn: ref(false), + userName: ref('Ada'), + toggle() { + this.loggedIn(!this.loggedIn()) + }, + }, + { selector: '#auth-island' }, +) +``` + +### 2) Large list with stable keys and cheap row expressions + +```html +
      +
    • + + #{{ i }} + +
    • +
    +``` + +Pattern: + +1. `:key="row.id"` for stable identity. +2. Keep row expressions simple (`r-text` + direct paths). +3. Avoid heavy object construction inside row template expressions. + +### 3) Dynamic component template switching (`:is`) + +```html +
    +``` + +```ts +createApp({ + currentView: ref('UserView'), + selectedItem: ref({ id: 1, name: 'Ada' }), + components: { UserView, AuditView }, +}) +``` + +Use this when one region must swap templates/components based on state, while keeping parent bindings simple. + +### 4) Slot template evaluated in parent context + +```ts +const Shell = createComponent('
    ', { + context: (head) => { + head.enableSwitch = true + return {} + }, +}) +``` + +```html + +

    {{ parentMessage }}

    +
    +``` + +With `head.enableSwitch = true`, slot expressions can resolve against parent context naturally. + +### 5) Interpolation-off region for literal template text + +```ts +const cfg = new RegorConfig() +cfg.useInterpolation = false +createApp( + { msg: ref('x') }, + { element: root, template: '
    {{ msg }}
    ' }, + cfg, +) +``` + +or local skip: + +```html +
    +  {{ this stays literal }}
    +
    +``` + +Use these when docs/snippets must display `{{ ... }}` without binding. + +## See Also + +1. [Directives](../directives) +2. [Reactivity](./reactivity) +3. [Components](./components) +4. [r-for](../directives/r-for) diff --git a/docs-site/src/content/docs/guide/troubleshooting.md b/docs-site/src/content/docs/guide/troubleshooting.md new file mode 100644 index 0000000..a712394 --- /dev/null +++ b/docs-site/src/content/docs/guide/troubleshooting.md @@ -0,0 +1,180 @@ +--- +title: Troubleshooting +sidebar: + order: 90 +--- + +Use this page as a symptom -> cause -> fix reference based on current Regor runtime behavior. + +## Quick Triage + +Before deep debugging: + +1. Verify root exists and app actually mounted. +2. Verify template is the one you think is mounted (selector/element/string/json path). +3. Verify binding channel is correct (`:x` vs `:context` vs `r-bind="{...}"`). +4. Verify scope/hook usage (`useScope` for composables). +5. Verify teardown timing (`unmount` + `drainUnbind` in tests). + +## App Does Not Mount + +Symptom: + +1. `createApp` throws and nothing renders. + +Likely cause: + +1. Missing root element. + +Runtime error: + +1. `createApp can't find root element...` + +Fix: + +```ts +createApp(ctx, { element: document.querySelector('#app')! }) +// or +createApp(ctx, { selector: '#app' }) +``` + +If you use `createApp(ctx, '
    ...
    ')`, Regor expects a real `#app` in DOM. + +## `onMounted` / `onUnmounted` Throws + +Symptom: + +1. Error about composables requiring scope. + +Likely cause: + +1. Hook registered outside active scope. + +Runtime error: + +1. `Use composables in scope. usage: useScope(() => new MyApp())` + +Fix: + +```ts +createApp( + useScope(() => { + onMounted(() => {}) + onUnmounted(() => {}) + return { count: ref(0) } + }), + { element: root }, +) +``` + +## Interpolation Not Updating + +Symptom: + +1. `{{ expr }}` / `[[ expr ]]` appears as raw text. + +Likely causes: + +1. `config.useInterpolation = false` +2. Markup is under `r-pre` +3. Expression resolves to missing path/value + +Fix checklist: + +1. Enable interpolation (`RegorConfig.useInterpolation = true`) when needed. +2. Remove `r-pre` from dynamic subtree. +3. Confirm expression resolves in active context. + +## Component Inputs Not Reaching Context + +Most confusion here comes from input channel mismatch. + +Correct routing: + +1. `:x` / `.x` / `r-bind:x`: + works as component prop only when `x` is declared in `props`. +2. `:context` / `r-context`: + object-style component input channel. +3. `r-bind="{...}"` (object form): + attribute channel on component hosts (fallthrough), not `head.props` object assignment. + +If component sets `head.autoProps = false`, you must map `head.props` manually in `context(head)`. + +## Parent-Child Ref Sync Feels Wrong + +Check `head.entangle` semantics: + +1. `head.entangle = true`: + ref-to-ref inputs are two-way entangled. +2. `head.entangle = false`: + ref-to-ref inputs are snapshot-only (no persistent sync). + +Also check whether parent sends ref source vs primitive expression: + +1. `:context="{ x: someRef }"` behaves differently from `:context="{ x: someRef + 1 }"`. + +## Slot Expression Uses Wrong Context + +Symptom: + +1. Slot content appears to evaluate against child context instead of parent. + +Fix: + +1. Enable slot context switching in component: + +```ts +context: (head) => { + head.enableSwitch = true + return {} +} +``` + +Then verify slot naming matches (`name` / `#name`). + +## `r-for` Updates Look Broken + +Checklist: + +1. Use stable identity keys (`:key="row.id"` or `key="row.id"`). +2. Ensure key is unique and stable across updates. +3. Avoid mutating key identity unexpectedly between renders. + +## Cleanup / Memory / Test Flakiness + +Know the teardown difference: + +1. `app.unmount()` removes root node and queues unbind flush. +2. `app.unbind()` detaches bindings without removing DOM. + +In tests, if assertions depend on cleanup side effects: + +```ts +app.unmount() +await drainUnbind() +``` + +## Warnings You Should Not Ignore + +Frequent warning types and meaning: + +1. Missing binding expression: + directive attribute exists but expression is empty. +2. Invalid `r-for` expression: + loop syntax is malformed. +3. Binding requires object expression: + directive expected object input (for example object-style binding paths). +4. Key is empty: + dynamic key path resolved to empty key. +5. Model requires ref / unsupported element: + `r-model` target is invalid for current element/source. + +## When Reporting an Issue + +Include: + +1. Minimal template + context snippet. +2. Expected result vs actual result. +3. Which channel you used (`:x`, `r-bind:x`, `:context`, `r-context`, `r-bind="{...}"`). +4. Whether problem persists after `unmount` + `await drainUnbind()`. +5. Regor version + runtime environment. diff --git a/docs-site/src/content/docs/guide/typescript.md b/docs-site/src/content/docs/guide/typescript.md new file mode 100644 index 0000000..abb18ab --- /dev/null +++ b/docs-site/src/content/docs/guide/typescript.md @@ -0,0 +1,110 @@ +--- +title: TypeScript +sidebar: + order: 40 +--- + +Regor has strong TypeScript support across app context, component context, and component head typing. + +## Why this matters + +You can model app and component state with real TypeScript types (interfaces/classes), and keep runtime template syntax concise. + +In template expressions, Regor resolves refs automatically, so you write `user.name` instead of `user.name.value` or `user.name()`. + +## Typed app context with interfaces + +```ts +import { createApp, html, type IRegorContext, type Ref, ref } from 'regor' + +interface TodoItem { + id: number + label: string +} + +interface MyApp extends IRegorContext { + title: Ref + items: Ref +} + +createApp( + { + title: ref('Todos'), + items: ref([{ id: 1, label: 'Ship' }]), + }, + { + element: document.querySelector('#app')!, + template: html`

    +
  • `, + }, +) +``` + +## Typed class app context with `useScope` + +```ts +import { createApp, html, ref, useScope } from 'regor' + +class BoardApp { + query = ref('') + count = ref(0) + increment = () => this.count(this.count() + 1) +} + +createApp( + useScope(() => new BoardApp()), + { + element: document.querySelector('#app')!, + template: html` + +

    {{ query }} - {{ count }}

    `, + }, +) +``` + +## Typed component props and context with `ComponentHead` + +```ts +import { ComponentHead, createComponent, html, type Ref, ref } from 'regor' + +type CardProps = { + title: Ref +} + +class CardContext { + title: Ref + selected = ref(false) + + constructor(head: ComponentHead) { + this.title = head.props.title + } +} + +const Card = createComponent( + html`
    +

    +
    `, + { + props: ['title'], + context: (head) => new CardContext(head), + }, +) +``` + +```html + +``` + +## Typed class components and behavior controls + +You can type `head` in class constructors and use runtime behavior flags intentionally: + +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. + +## See Also + +1. [Components](./components) +2. [createComponent API](../api/createComponent) +3. [createApp API](../api/createApp) diff --git a/docs-site/src/content/docs/index.mdx b/docs-site/src/content/docs/index.mdx index b94eb46..458da3b 100644 --- a/docs-site/src/content/docs/index.mdx +++ b/docs-site/src/content/docs/index.mdx @@ -2,19 +2,20 @@ title: Regor template: splash sidebar: - order: 1 + order: 1 hero: - title: "Build reactive UIs" + title: "Reactive UI without framework lock-in" tagline: | - A modern, reactive UI framework for web & desktop. - zero build step, zero virtual DOM, - with secure, flexible reactivity. + Regor gives you direct DOM control, strong reactivity primitives, + and runtime-first flexibility for real applications. image: file: ../../assets/regor-logo.svg actions: - - text: "Get Started" + - text: "Start Building" icon: right-arrow link: ./getting-started + - text: "Read the Guide" + link: ./guide - text: "GitHub" icon: external variant: minimal @@ -26,51 +27,60 @@ hero: import { CardGrid, Card, LinkCard } from '@astrojs/starlight/components'; -Regor empowers developers to craft high-performance, reactive user interfaces effortlessly. Whether you're building a single-page app, desktop GUI, or hybrid solution, Regor’s intuitive API and uncompromising performance let you focus on what matters: your users. +Regor is a modern reactive UI library for teams that want **power and control** instead of rigid framework constraints. + +It supports: + +1. Runtime binding over existing markup. +2. Fine-grained reactive primitives (`ref`, `sref`, computed, observers). +3. Component and context models that stay flexible. +4. Direct JavaScript/DOM escape hatches where performance or integration requires it. - - Template conventions familiar to Vue.js developers for an instant productivity boost. + + Bind dynamic behavior directly on existing DOM. Regor does not require full-template ownership. - - Full type-safety with zero workarounds—define components and props in TS. + + Use `ref`, `sref`, `computed`, `watchEffect`, observers, and batch updates to model state precisely. - - Run directly in the browser or Node.js—no bundler, no compilation delays. + + Rich directives for lists, conditionals, events, text/HTML binding, forms, dynamic components, and attribute orchestration. - - Built-in JavaScript VM enforces CSP-friendly, sandboxed template evaluation. + + Build components in TypeScript with practical context behavior and dynamic composition. - - Granular state control via ref, computed, watchEffect, and custom hooks. + + Use npm packages, CDN ESM, or global build. Choose the workflow that fits your deployment model. - - Partially render any part in the raw html. + + Stay close to the DOM and optimize hot paths with full visibility instead of opaque rendering internals. - - Mount multiple app contexts in the same DOM tree—dynamic components on the fly. + + Adopt Regor incrementally in legacy or mixed-stack applications. - Seamlessly coexists with other DOM-manipulating libraries and micro-frontends. + Integrates cleanly with external DOM logic and micro-frontend environments. -## Quick Links +## Start Here - - - - - + + + + + + -> **Note:** Regor is open-source. Contributions and feedback are welcome! +## Community and Contribution - - \ No newline at end of file + + + diff --git a/docs-site/src/content/docs/overview.md b/docs-site/src/content/docs/overview.md index dabc091..75ca31f 100644 --- a/docs-site/src/content/docs/overview.md +++ b/docs-site/src/content/docs/overview.md @@ -1,63 +1,79 @@ --- title: Overview sidebar: - order: 3 + order: 2 --- -Regor is a reactive UI framework built for developers who want direct control over the DOM, expressive runtime behavior, and strong TypeScript ergonomics. +Regor is a reactive UI library designed for engineers who want: -It is designed to be practical, not restrictive. +1. Direct control over runtime behavior. +2. A powerful template/directive model. +3. TypeScript-friendly reactivity primitives. +4. Freedom to optimize with plain JavaScript and DOM when needed. -## What Regor Optimizes For +Regor is intentionally practical. It does not force one rigid rendering architecture for every use case. -1. **Control over abstraction** - Regor does not force one universal rendering model for every case. You can use high-level directives where they help and drop to direct JavaScript/DOM logic where needed. +## Why Teams Choose Regor -2. **Runtime flexibility** - Regor can bind existing HTML markup and progressively enhance server-rendered or mixed-stack pages without requiring a full ownership rewrite. +### 1. Runtime flexibility without lock-in -3. **Composable reactivity** - Regor provides `ref`, `sref`, computed and observer-based primitives so you can choose the right level of reactivity per feature. +Regor can bind existing markup and progressively enhance real pages. You can adopt it incrementally instead of rewriting everything. -4. **TypeScript-first development** - Components, props, and reactive state can be modeled in TypeScript directly, with predictable runtime behavior. +### 2. Reactivity that stays explicit -5. **Security-aware runtime evaluation** - Regor supports CSP-friendly environments and practical secure evaluation scenarios for runtime expressions. +You get `ref`, `sref`, `computed`, `watchEffect`, `observe`, and batching primitives. State flows are explicit and composable. -## Design Philosophy +### 3. Directive model with real depth -Regor intentionally avoids over-constraining architecture choices. +Regor supports rich directive workflows: -That means: +1. Conditional rendering (`r-if`, `r-else-if`, `r-else`, `r-show`). +2. List rendering (`r-for` with keys, table use cases, nested structures). +3. Attribute/property/class/style/event binding (`r-bind`, `.prop`, `.camel`, `:class`, `:style`, `@event`). +4. Form binding (`r-model`) and dynamic components (`:is`, `:context`). -1. Component and context patterns are flexible. -2. Parent/child interaction is not artificially limited to one rigid style. -3. Framework internals aim to stay understandable and hackable for real-world optimization work. +### 4. Excellent fit for mixed environments -## Tradeoff Model +Regor works well when part of your DOM is owned by other systems, or when performance-critical sections need custom handling. -Regor prioritizes flexibility and control. In highly generic, binding-dense scenarios, that flexibility can add runtime overhead compared to highly specialized pipelines. +## Core Philosophy -In return, Regor gives you: +Regor’s philosophy is: -1. Better escape hatches for custom optimization. -2. Better fit for progressive enhancement and mixed DOM ownership. -3. A development model that does not force one-size-fits-all constraints. +1. Keep defaults productive. +2. Keep escape hatches available. +3. Keep internals understandable. +4. Keep control in developers’ hands. -## When Regor Is a Strong Fit +## What Regor Is Not Trying To Be -Regor is especially strong when you need: +Regor is not built around artificial constraints to optimize only one benchmark shape. It is built for real product constraints where integration flexibility matters. -1. Runtime binding over existing markup. -2. Fine-grained control over component and context behavior. -3. Custom domain-specific rendering optimizations. -4. Direct integration with other DOM-manipulating systems. +If your app needs: -## Next Steps +1. Existing-markup binding. +2. Runtime composition. +3. Context flexibility. +4. Controlled optimization paths. -1. Read [Getting Started](/getting-started). -2. Explore [Directives](/directives). -3. Use the [Performance Guide](/performance) for profiling and optimization strategy. +Regor is a strong fit. + +## Practical Tradeoffs + +Regor favors flexibility and control. In very binding-dense paths, runtime cost can grow with total DOM + binding volume. This is normal for highly dynamic runtime systems. + +The upside is that Regor keeps optimization options open: + +1. Reduce binding density where it matters. +2. Use stable keying and list strategies. +3. Drop to targeted custom logic in hot sections. +4. Keep the rest of the app high-level and maintainable. + +## Recommended Reading Order + +1. [Getting Started](./getting-started) +2. [Guide](./guide) +3. [Directives](./directives) +4. [API Reference](./api) +5. [Performance Guide](./guide/performance) -[Back to the main](/) diff --git a/docs-site/src/content/docs/performance.md b/docs-site/src/content/docs/performance.md deleted file mode 100644 index 844688c..0000000 --- a/docs-site/src/content/docs/performance.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -title: Performance Guide -sidebar: - order: 5 ---- - -This page summarizes practical performance guidance for Regor apps, based on real benchmark work and profiling. - -## What Matters Most - -Performance is usually dominated by: - -1. Number of DOM nodes created/updated. -2. Number of reactive bindings attached. -3. Amount of work done per binding (`r-text`, `:class`, `:style`, etc.). -4. List size and list mutation strategy (`r-for` + `:key`). - -In large templates, total cost is almost always cumulative. Many small costs can add up. - -## Benchmark Correctness Checklist - -Before comparing Regor with another framework, ensure: - -1. Same rendered output and same feature set on both sides. -2. Same timing boundary on both sides (for example, both measured with one `requestAnimationFrame` after mount, or neither). -3. Warmup runs are excluded from reported numbers. -4. A/B alternating order is used per sample to reduce bias. -5. Median and P90 are reported, not single-run results. - -If timing boundaries differ, benchmark numbers are not comparable. - -## Interpreting Numbers - -Use this rough UX scale for one interaction: - -1. Under `100ms`: usually feels instant. -2. `100ms` to `300ms`: noticeable, usually acceptable. -3. Over `300ms`: often perceived as slow. - -Absolute numbers matter more than framework-vs-framework deltas if user experience is already smooth on target devices. - -## Large Lists: Recommended Strategy - -If you render many rows: - -1. Prefer windowing/pagination/chunking over fully reactive huge lists. -2. Keep row templates minimal. -3. Avoid expensive computed object bindings per row where possible. -4. Use stable keys (`:key`) for predictable updates. - -## Regor-Specific Strength - -Regor is designed for runtime flexibility: - -1. Bind to existing HTML markup in place. -2. Mix framework bindings with direct DOM/JS escape hatches in critical paths. -3. Keep control when you need custom, domain-specific optimization. - -This flexibility is valuable in progressive enhancement and mixed-stack environments. - -## Practical Optimization Order - -When a page feels slow, optimize in this order: - -1. Reduce rendered nodes. -2. Reduce active bindings per node. -3. Simplify hot directives in repeated rows. -4. Profile and fix the top one or two real hotspots. -5. Only then consider deeper framework-internal changes. - -## When Not To Optimize - -If your measured path is already in an acceptable UX range, prioritize maintainability and product features. Complexity-heavy hot paths are only worth it when they solve a real user-visible bottleneck. diff --git a/package.json b/package.json index 04b1e98..9f0384b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "regor", - "version": "1.3.4", + "version": "1.3.5", "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/api/types.ts b/src/api/types.ts index 0a9685b..f47e260 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -326,29 +326,82 @@ export type OnUnmounted = () => void export interface CreateComponentOptions< TContext extends IRegorContext | object = IRegorContext, > { + /** + * Enables interpolation transform inside the component template. + * + * When `true` (default), text like `{{ expr }}` / `[[ expr ]]` is converted + * to directive bindings during component template preparation. + * + * Set to `false` when the template should keep interpolation markers as plain text. + */ useInterpolation?: boolean + /** + * Regor configuration used while creating this component. + * + * Useful when the component must use a specific config instance + * (custom directives, global context, registered components). + */ config?: RegorConfig /** - * A function that defines the Regor context for the component. + * Factory that creates the component context. + * + * `head` contains incoming parent-bound values (`head.props`) and mount controls. + * + * Example: + * ```ts + * context(head) { + * return { + * title: head.props.title ?? 'Untitled', + * close: () => head.emit('close', { reason: 'user' }), + * } + * } + * ``` */ context?(head: ComponentHead): TContext + /** + * Controls attribute fallthrough from component host to component root. + * + * - `true` (default): non-prop attributes like `class`, `style`, `id` are inherited. + * - `false`: host attributes are not forwarded automatically. + */ inheritAttrs?: boolean /** - * Notes on component props: - * The props defined in the props list can be used with :foo or r-bind:foo syntax. - * ` + * ``` + * + * Note: + * `:context="{ ... }"` can assign arbitrary component fields even when + * they are not declared in `props`. */ props?: string[] - /** The default name of the component. - * It is required if the component is registered using the Regor config.addComponent method. - * It is not required if the component being registered in app or component scope. */ + /** + * Default component name used by global registration (`config.addComponent(...)`). + * + * Required for global registration. + * Optional when component is provided via app/component local `components` context. + * + * Example: + * ```ts + * const Card = createComponent('
    ...
    ', { defaultName: 'CardView' }) + * cfg.addComponent(Card) + * // usable as + * ``` + */ defaultName?: string } diff --git a/src/app/ComponentHead.ts b/src/app/ComponentHead.ts index f95f04b..926b8f8 100644 --- a/src/app/ComponentHead.ts +++ b/src/app/ComponentHead.ts @@ -2,34 +2,123 @@ import { type IRegorContext } from '../api/types' import { removeNode } from '../cleanup/removeNode' import { callUnmounted } from '../composition/callUnmounted' +/** + * Runtime metadata passed to a component's `context(head)` factory. + * + * `ComponentHead` gives component authors controlled access to: + * - incoming values from parent (`head.props`) + * - component mount boundaries (`head.start`, `head.end`) + * - parent-context event bridge (`head.emit(...)`) + * - optional behavior toggles (`head.autoProps`, `head.entangle`, `head.enableSwitch`) + * + * Typical usage: + * ```ts + * const Card = createComponent({ + * template: `

    `, + * props: ['title'], + * context(head) { + * // read parent values + * const initialTitle = head.props.title + * + * // optional event to parent/listener + * const close = () => head.emit('close', { reason: 'user' }) + * + * return { title: initialTitle, close } + * }, + * }) + * ``` + */ export class ComponentHead< TContext extends IRegorContext | object = IRegorContext, > { + /** + * Values provided by parent for this component instance. + * + * Sources: + * - declared props via `props: ['foo']` + attribute binding (`:foo="..."`) + * - object binding via `:context="{ ... }"` + */ props: TContext + /** + * Comment node that marks the beginning of this mounted component block. + * Advanced use only. + */ start: Comment + /** + * Comment node that marks the end of this mounted component block. + * Advanced use only. + */ end: Comment + /** + * Captured context chain used by this component instance. + * Used internally for lifecycle/unmount behavior. + */ ctx: IRegorContext[] - /** Automatically assigns properties defined in the :props binding to the component context when enabled. If disabled, props should be manually assigned using head.props. - * Default: true */ + /** + * Controls whether Regor should automatically apply incoming `head.props` + * values to the component context after `context(head)` returns. + * + * Think of it as "auto wire parent inputs into my component fields". + * + * - `true` (default): + * - If a key exists in `head.props` but does not exist on the object + * returned by `context(head)`, Regor adds that key to component context. + * - Existing ref fields can receive incoming values automatically. + * - Ref-to-ref inputs can be entangled when `head.entangle` is enabled. + * - `false`: + * - Regor does not auto-apply props. + * - You fully control mapping manually inside `context(head)`. + * + * Use `false` when you need strict custom mapping/validation/transforms + * before any value touches component state. + * + * "Missing key" is always checked against the returned component context object. + * + * Example (auto add): + * ```ts + * // Parent passes: :context="{ badge: 'pro' }" + * context(head) { + * // Returned context has no "badge" key: + * return { name: ref('Ada') } + * } + * // Resulting component context becomes: + * // { name: ref('Ada'), badge: 'pro' } + * ``` + * + * Example: + * ```ts + * context(head) { + * head.autoProps = false + * const title = ref((head.props.title as string) ?? 'Untitled') + * return { title } + * } + * ``` + */ autoProps = true - /** When both autoProps and entangle are enabled, - * the refs defined in the component context (without using head.props) - * become entangled with the head.props refs. (parent[ref] `<==>` component[ref]) - * This means that changes to parent[ref] reflect in component[ref], and vice versa. - * Disable this flag to isolate refs created within the component context. - * Default: true */ + /** + * Enables two-way ref linking between incoming props and component context + * when `autoProps` is also enabled. + * + * - `true` (default): parent and component refs stay synchronized. + * - `false`: component keeps local ref isolation. + */ entangle = true - /** enables slot context switch to the parent - * Default: false */ + /** + * Enables slot context switch behavior for advanced slot scenarios. + * Default: `false`. + */ enableSwitch = false - /** A callback invoked after auto props get assigned to the component context. */ + /** + * Optional hook called after automatic prop assignment completes. + * Useful when post-assignment normalization is needed. + */ onAutoPropsAssigned?: () => void /** @@ -51,13 +140,29 @@ export class ComponentHead< this.end = end } - /** use arrow syntax to be called without using head.emit.bind(head) in Binder.ts. */ + /** + * Emits a custom DOM event from the component host element. + * + * Example: + * ```ts + * head.emit('saved', { id: 42 }) + * ``` + * + * Parent markup can listen via regular event binding: + * ```html + * + * ``` + */ emit = (event: string, args: Record): void => { this.__element.dispatchEvent( new CustomEvent>(event, { detail: args }), ) } + /** + * Unmounts this component instance by removing nodes between `start` and `end` + * and calling unmount lifecycle handlers for captured contexts. + */ unmount(): void { let next = this.start.nextSibling const end = this.end diff --git a/src/app/RegorConfig.ts b/src/app/RegorConfig.ts index 07ad47a..052094f 100644 --- a/src/app/RegorConfig.ts +++ b/src/app/RegorConfig.ts @@ -132,6 +132,7 @@ export class RegorConfig { [`${prefix}show`]: showDirective, [`${prefix}model`]: modelDirective, ':style': styleDirective, + [`${prefix}style`]: styleDirective, [`${prefix}bind:style`]: styleDirective, ':class': classDirective, [`${prefix}bind:class`]: classDirective, @@ -151,8 +152,8 @@ export class RegorConfig { pre: `${prefix}pre`, inherit: `${prefix}inherit`, text: `${prefix}text`, - props: ':props', - propsOnce: ':props-once', + context: ':context', + contextAlias: `${prefix}context`, bind: `${prefix}bind`, on: `${prefix}on`, keyBind: ':key', diff --git a/src/app/createApp.ts b/src/app/createApp.ts index 192ee7c..b3a9905 100644 --- a/src/app/createApp.ts +++ b/src/app/createApp.ts @@ -20,6 +20,43 @@ import { preprocess } from './preprocess-template' import { RegorConfig } from './RegorConfig' import { toFragment } from './toFragment' +/** + * Creates and mounts a Regor application on an existing DOM root. + * + * The app can bind directly to existing markup or replace root content with + * a provided `template`/`json` before binding. + * + * @typeParam TRegorContext - Root app context type. + * @param context - App context object (or `useScope(... )` result). + * @param template - Mount target + optional template source. + * @param config -Optional Regor config. Uses `RegorConfig.getDefault()` when omitted. + * + * @returns App handle with: + * - `context`: the same context instance used for binding + * - `unbind()`: removes Regor observers/listeners from the root + * - `unmount()`: removes the root element from DOM + * + * @example + * ```ts + * const root = document.getElementById('app')! + * const app = createApp( + * { count: ref(0) }, + * { + * element: root, + * template: ``, + * }, + * ) + * ``` + * + * @example + * ```ts + * // Bind against existing markup without replacing it. + * const app = createApp( + * { user: ref('Ada') }, + * { selector: '#header-island' }, + * ) + * ``` + */ export const createApp = < TRegorContext extends IRegorContext | object = IRegorContext, >( diff --git a/src/app/createComponent.ts b/src/app/createComponent.ts index a0ae46c..fbab18f 100644 --- a/src/app/createComponent.ts +++ b/src/app/createComponent.ts @@ -13,6 +13,43 @@ import { RegorConfig } from './RegorConfig' import { toFragment } from './toFragment' import { toJsonTemplate } from './toJsonTemplate' +/** + * Creates a reusable Regor component definition. + * + * `createComponent` prepares a template once, then Regor clones/binds it for each + * component instance in the app. + * + * @typeParam TContext - Component context type. + * @param template Component template source: + * - inline HTML string + * - `Template` object (`template`, `element`, `selector`, or `json`) + * @param options Component options (`context`, `props`, `inheritAttrs`, etc.). + * You can also pass `string[]` as shorthand for `props`. + * + * @returns Component definition usable in app/component `components`. + * + * @example + * ```ts + * const UserCard = createComponent( + * `

    `, + * { + * props: ['name'], + * context: (head) => ({ + * name: head.props.name ?? 'Anonymous', + * }), + * }, + * ) + * ``` + * + * @example + * ```ts + * // Props shorthand: + * const CounterLabel = createComponent( + * ``, + * ['value'], + * ) + * ``` + */ export const createComponent = < TContext extends IRegorContext | object = IRegorContext, >( diff --git a/src/bind/ComponentBinder.ts b/src/bind/ComponentBinder.ts index 47f15ba..f7eb819 100644 --- a/src/bind/ComponentBinder.ts +++ b/src/bind/ComponentBinder.ts @@ -16,8 +16,7 @@ import { isNullOrWhitespace } from '../common/is-what' import { callMounted } from '../composition/callMounted' import { callUnmounted } from '../composition/callUnmounted' import { useScope } from '../composition/useScope' -import { propsDirective } from '../directives/props' -import { propsOnceDirective } from '../directives/props-once' +import { contextDirective } from '../directives/context' import { singlePropDirective } from '../directives/single-prop' import { entangle } from '../reactivity/entangle' import { isRef } from '../reactivity/isRef' @@ -114,8 +113,8 @@ export class ComponentBinder { const endOfComponent = new Comment(' end component: ' + component.tagName) componentParent.insertBefore(startOfComponent, component) component.remove() - const propsName = binder.__config.__builtInNames.props - const propsOnceName = binder.__config.__builtInNames.propsOnce + const contextName = binder.__config.__builtInNames.context + const contextAliasName = binder.__config.__builtInNames.contextAlias const bindName = binder.__config.__builtInNames.bind const getProps = ( @@ -123,13 +122,14 @@ export class ComponentBinder { capturedContext: any[], ): Record => { const props: Record = {} - const hasProps = component.hasAttribute(propsName) - const hasPropsOnce = component.hasAttribute(propsOnceName) + const hasContext = component.hasAttribute(contextName) parser.__scoped(capturedContext, () => { parser.__push(props) - if (hasProps) binder.__bind(propsDirective, component, propsName) - if (hasPropsOnce) - binder.__bind(propsOnceDirective, component, propsOnceName) + if (hasContext) { + binder.__bind(contextDirective, component, contextName) + } else if (component.hasAttribute(contextAliasName)) { + binder.__bind(contextDirective, component, contextAliasName) + } let definedProps = registeredComponent.props if (!definedProps || definedProps.length === 0) return @@ -192,12 +192,33 @@ export class ComponentBinder { if (key in componentCtx) { const compValue = componentCtx[key] if (compValue === propsValue) continue - // the component author didn't provide property using head.props - // if entangle enabled, entangle the props[ref] to the componentCtx[ref] to provide parent-child data sync. - if (head.entangle && isRef(compValue) && isRef(propsValue)) { - addUnbinder(startOfComponent, entangle(propsValue, compValue)) + if (isRef(compValue)) { + // Existing component field is a ref: + // route incoming value into that ref instead of replacing the field. + if (isRef(propsValue)) { + // Ref -> ref input: + // - entangle=true: keep both refs synchronized + // - entangle=false: take one-time snapshot from parent ref + // the component author didn't provide property using head.props + // if entangle enabled, entangle the props[ref] to the componentCtx[ref] to provide parent-child data sync. + if (head.entangle) { + addUnbinder( + startOfComponent, + entangle(propsValue, compValue), + ) + } else { + compValue(propsValue()) + } + } else { + // Primitive/object input into existing component ref: + // apply value to ref target. + compValue(propsValue) + } + continue } + // Existing non-ref field is intentionally preserved as component-owned state. } else componentCtx[key] = propsValue + // Key does not exist on component context yet; auto-add from inputs. } head.onAutoPropsAssigned?.() } @@ -320,7 +341,8 @@ export class ComponentBinder { if (!inheritor) return for (const attrName of component.getAttributeNames()) { - if (attrName === propsName || attrName === propsOnceName) continue + if (attrName === contextName || attrName === contextAliasName) + continue const value = component.getAttribute(attrName) as string if (attrName === 'class') { inheritor.classList.add(...value.split(' ')) diff --git a/src/directives/props.ts b/src/directives/context.ts similarity index 95% rename from src/directives/props.ts rename to src/directives/context.ts index 6c92fdc..8c197a8 100644 --- a/src/directives/props.ts +++ b/src/directives/context.ts @@ -6,7 +6,7 @@ import { isRef } from '../reactivity/isRef' /** * @internal */ -export const propsDirective: Directive = { +export const contextDirective: Directive = { collectRefObj: true, onBind: (_: HTMLElement, parseResult: ParseResult): Unbinder => { const stopObserving = observe( diff --git a/src/directives/props-once.ts b/src/directives/props-once.ts deleted file mode 100644 index 0fbce61..0000000 --- a/src/directives/props-once.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { type Directive, type ParseResult, type Unbinder } from '../api/types' -import { isObject } from '../common/is-what' -import { isRef } from '../reactivity/isRef' - -/** - * @internal - */ -export const propsOnceDirective: Directive = { - collectRefObj: true, - once: true, - onBind: (_: HTMLElement, parseResult: ParseResult): Unbinder => { - const value = parseResult.value() - const ctx = parseResult.context - const obj = value[0] - if (!isObject(obj)) { - return () => {} - } - for (const item of Object.entries(obj)) { - const key = item[0] - const value = item[1] - const ctxKey = ctx[key] - if (ctxKey === value) continue - if (isRef(ctxKey)) { - ctxKey(value) - } else { - ctx[key] = value - } - } - return () => {} - }, -} diff --git a/tests/app/class-context.spec.ts b/tests/app/class-context.spec.ts index ad191c4..dec8969 100644 --- a/tests/app/class-context.spec.ts +++ b/tests/app/class-context.spec.ts @@ -356,6 +356,14 @@ test('class component autoProps entangle can sync or isolate refs', () => { ) as HTMLInputElement | null if (!entangled || !isolated) throw new Error('missing test inputs') + // Entangled input starts from parent and keeps two-way sync. + expect(app.context.sharedA()).toBe('root-a') + expect(entangled.value).toBe('root-a') + + // Isolated input takes an initial snapshot from parent, but does not entangle. + expect(app.context.sharedB()).toBe('root-b') + expect(isolated.value).toBe('root-b') + app.context.sharedA('from-parent') expect(entangled.value).toBe('from-parent') @@ -364,11 +372,16 @@ test('class component autoProps entangle can sync or isolate refs', () => { expect(app.context.sharedA()).toBe('from-child') app.context.sharedB('from-parent') - expect(isolated.value).toBe('local-isolated') + expect(isolated.value).toBe('root-b') isolated.value = 'child-local' isolated.dispatchEvent(new Event('input')) expect(app.context.sharedB()).toBe('from-parent') + + // Parent can still change independently after child edit without forcing child value. + app.context.sharedB('parent-after-child') + expect(isolated.value).toBe('child-local') + expect(app.context.sharedB()).toBe('parent-after-child') }) test('nested class components run lifecycle hooks and unmount cleanly', async () => { @@ -456,7 +469,7 @@ test('class scope cleans up watchEffect/observe/observeMany on unmount', async ( expect(observerCount(app.context.b)).toBe(0) }) -test('class component supports :props and :props-once with class ref fields', () => { +test('class component supports :context with class ref fields', () => { class LabelContext { liveLabel = ref('live-local') onceLabel = ref('once-local') @@ -483,8 +496,7 @@ test('class component supports :props and :props-once with class ref fields', () { element: root, template: html``, }, ) diff --git a/tests/app/createComponent.spec.ts b/tests/app/createComponent.spec.ts index 14aa5b2..5b30bd8 100644 --- a/tests/app/createComponent.spec.ts +++ b/tests/app/createComponent.spec.ts @@ -41,12 +41,14 @@ test('should render components with reactive properties', () => { - - + - + + `, }, @@ -55,7 +57,7 @@ test('should render components with reactive properties', () => { root.innerHTML, html`
    -
    prop-1-prop-2
    +
    My Property 1-prop-2
    ok-prop-2
    @@ -67,14 +69,14 @@ test('should render components with reactive properties', () => {
    prop-1-prop-2
    - +
    ok-ok
    ok-ok
    - +
    prop-1-prop-2
    @@ -85,7 +87,7 @@ test('should render components with reactive properties', () => { root.innerHTML, html`
    -
    prop-1-prop-2
    +
    My Property 1-prop-2
    yes-prop-2
    @@ -96,14 +98,14 @@ test('should render components with reactive properties', () => {
    prop-1-prop-2
    - +
    yes-yes
    yes-yes
    - +
    prop-1-prop-2
    @@ -148,6 +150,304 @@ test('should resolve camelCase dynamic prop bindings on components', () => { ) }) +test('component binding matrix: declared props, undeclared attrs, :context, and r-bind object', () => { + const root = document.createElement('div') + + const demoComponent = createComponent( + html`
    +

    {{ title }}

    +

    {{ extra }}

    +

    {{ declaredOnly }}

    +
    `, + { + context: () => ({ + title: ref('title-default'), + extra: ref('extra-default'), + declaredOnly: ref('declared-default'), + }), + props: ['title', 'declaredOnly'], + }, + ) + + const app = createApp( + { + components: { demoComponent }, + titleSrc: ref('title-1'), + extraSrc: ref('extra-1'), + declaredSrc: ref('declared-1'), + }, + { + element: root, + template: html`
    + + + + + +
    `, + }, + ) + + const text = (id: string, cls: string): string => + ( + root.querySelector(`#${id} .${cls}`) as HTMLElement | null + )?.textContent?.trim() ?? '' + const host = (id: string): HTMLElement | null => + root.querySelector(`#${id}`) as HTMLElement | null + + expect(text('declared', 'title')).toBe('title-1') + expect(text('declared', 'declared')).toBe('declared-1') + expect(text('declared', 'extra')).toBe('extra-default') + + expect(text('undeclared', 'title')).toBe('title-default') + expect(text('undeclared', 'extra')).toBe('extra-default') + expect(host('undeclared')?.getAttribute('extra')).toBe('extra-1') + + expect(text('context', 'title')).toBe('title-1') + expect(text('context', 'extra')).toBe('extra-1') + + expect(text('contextSecond', 'title')).toBe('title-1') + expect(text('contextSecond', 'extra')).toBe('extra-1') + + expect(text('bindObject', 'title')).toBe('title-default') + expect(text('bindObject', 'extra')).toBe('extra-default') + expect(host('bindObject')?.getAttribute('title')).toBe('title-1') + expect(host('bindObject')?.getAttribute('extra')).toBe('extra-1') + + app.context.titleSrc('title-2') + app.context.extraSrc('extra-2') + app.context.declaredSrc('declared-2') + + expect(text('declared', 'title')).toBe('title-2') + expect(text('declared', 'declared')).toBe('declared-2') + expect(text('declared', 'extra')).toBe('extra-default') + + expect(text('undeclared', 'title')).toBe('title-default') + expect(text('undeclared', 'extra')).toBe('extra-default') + expect(host('undeclared')?.getAttribute('extra')).toBe('extra-2') + + expect(text('context', 'title')).toBe('title-2') + expect(text('context', 'extra')).toBe('extra-2') + + expect(text('contextSecond', 'title')).toBe('title-2') + expect(text('contextSecond', 'extra')).toBe('extra-2') + + expect(text('bindObject', 'title')).toBe('title-default') + expect(text('bindObject', 'extra')).toBe('extra-default') + expect(host('bindObject')?.getAttribute('title')).toBe('title-2') + expect(host('bindObject')?.getAttribute('extra')).toBe('extra-2') +}) + +test('component supports r-context alias for :context', () => { + const root = document.createElement('div') + + const myComponent = createComponent( + html`
    + {{ title }} + {{ extra }} +
    `, + { + context: () => ({ + title: ref('title-default'), + extra: ref('extra-default'), + }), + }, + ) + + const app = createApp( + { + components: { myComponent }, + titleSrc: ref('title-1'), + extraSrc: ref('extra-1'), + }, + { + element: root, + template: html``, + }, + ) + + const getTitle = () => root.querySelector('.title')?.textContent?.trim() + const getExtra = () => root.querySelector('.extra')?.textContent?.trim() + + expect(getTitle()).toBe('title-1') + expect(getExtra()).toBe('extra-1') + + app.context.titleSrc('title-2') + app.context.extraSrc('extra-2') + expect(getTitle()).toBe('title-2') + expect(getExtra()).toBe('extra-2') +}) + +test('proof: :context can drive component state, r-bind object cannot', () => { + const root = document.createElement('div') + + const card = createComponent( + html`
    + {{ canEdit ? 'edit' : 'view' }} + {{ accessLevel }} +
    `, + { + context: () => ({ + canEdit: ref(false), + accessLevel: ref('guest'), + }), + }, + ) + + const app = createApp( + { + components: { card }, + flag: ref(true), + level: ref('admin'), + }, + { + element: root, + template: html`
    + + +
    `, + }, + ) + + const text = (id: string, cls: string): string => + ( + root.querySelector(`#${id} .${cls}`) as HTMLElement | null + )?.textContent?.trim() ?? '' + + expect(text('via-context', 'mode')).toBe('edit') + expect(text('via-context', 'level')).toBe('admin') + + // r-bind object only falls through as DOM attributes; component state stays default. + expect(text('via-bind', 'mode')).toBe('view') + expect(text('via-bind', 'level')).toBe('guest') + + app.context.flag(false) + app.context.level('member') + + expect(text('via-context', 'mode')).toBe('view') + expect(text('via-context', 'level')).toBe('member') + + expect(text('via-bind', 'mode')).toBe('view') + expect(text('via-bind', 'level')).toBe('guest') +}) + +test('proof: r-bind single-prop updates component state only for declared props', () => { + const root = document.createElement('div') + + const card = createComponent( + html`
    + {{ title }} + {{ note }} +
    `, + { + props: ['title'], + context: (head) => ({ + title: ref(head.props.title ?? 'title-default'), + note: ref('note-default'), + }), + }, + ) + + const app = createApp( + { + components: { card }, + titleSrc: ref('hello'), + noteSrc: ref('n1'), + }, + { + element: root, + template: html`
    + + +
    `, + }, + ) + + const text = (id: string, cls: string): string => + ( + root.querySelector(`#${id} .${cls}`) as HTMLElement | null + )?.textContent?.trim() ?? '' + + expect(text('declared', 'title')).toBe('hello') + expect(text('declared', 'note')).toBe('note-default') + + expect(text('object-undeclared', 'title')).toBe('title-default') + expect(text('object-undeclared', 'note')).toBe('note-default') + + app.context.titleSrc('world') + app.context.noteSrc('n2') + + expect(text('declared', 'title')).toBe('world') + expect(text('declared', 'note')).toBe('note-default') + + // Object-form r-bind stays as fallthrough and does not become component state. + expect(text('object-undeclared', 'title')).toBe('title-default') + expect(text('object-undeclared', 'note')).toBe('note-default') +}) + +test(':context keeps explicit ref(...) entries reactive in object literals', () => { + const root = document.createElement('div') + + const comp = createComponent( + html`
    + {{ live }} + {{ once }} +
    `, + { + context: () => ({ + live: ref(0), + once: ref(0), + }), + }, + ) + + const app = createApp( + { + components: { comp }, + count: ref(1), + }, + { + element: root, + template: html``, + }, + ) + + const getLive = () => root.querySelector('.live')?.textContent?.trim() + const getOnce = () => root.querySelector('.once')?.textContent?.trim() + + // live is passed as explicit ref(...), so it tracks parent updates. + expect(getLive()).toBe('2') + // once is primitive assignment to an existing local ref key. + expect(getOnce()).toBe('2') + + app.context.count(5) + expect(getLive()).toBe('6') + expect(getOnce()).toBe('2') +}) + test('should preserve context values when autoProps is enabled', () => { const root = document.createElement('div') @@ -500,6 +800,64 @@ test('slot switch contexts are released on unmount', async () => { expect(hasSwitch()).toBe(false) }) +test('enableSwitch controls whether slot bindings use parent or component context', () => { + const root = document.createElement('div') + + const shellNoSwitch = createComponent( + html`
    `, + { + context: () => ({ + message: ref('from-component-no-switch'), + }), + }, + ) + + const shellWithSwitch = createComponent( + html`
    `, + { + context: (head) => { + head.enableSwitch = true + return { + message: ref('from-component-with-switch'), + } + }, + }, + ) + + const app = createApp( + { + components: { shellNoSwitch, shellWithSwitch }, + message: ref('from-parent'), + }, + { + element: root, + template: html`
    + +

    {{ message }}

    +
    + +

    {{ message }}

    +
    +
    `, + }, + ) + + expect(root.querySelector('.no-switch')?.textContent?.trim()).toBe( + 'from-component-no-switch', + ) + expect(root.querySelector('.with-switch')?.textContent?.trim()).toBe( + 'from-parent', + ) + + app.context.message('from-parent-next') + expect(root.querySelector('.no-switch')?.textContent?.trim()).toBe( + 'from-component-no-switch', + ) + expect(root.querySelector('.with-switch')?.textContent?.trim()).toBe( + 'from-parent-next', + ) +}) + test('createComponent supports disabling interpolation and direct element children scan', () => { const rootEl = document.createElement('div') rootEl.innerHTML = '

    {{ msg }}

    ' @@ -673,8 +1031,7 @@ test('component inheritAttrs false keeps host attrs off child root', () => { { components: { comp } }, { element: root, - template: - '', + template: '', }, ) diff --git a/tests/bind/component-binder-edge.spec.ts b/tests/bind/component-binder-edge.spec.ts index 23fd270..7282cb0 100644 --- a/tests/bind/component-binder-edge.spec.ts +++ b/tests/bind/component-binder-edge.spec.ts @@ -113,7 +113,7 @@ test('component binder skips unsupported directive names even if option matches bindSpy.mockRestore() }) -test('component binder ignores :props/:props-once during attribute fallthrough transfer', () => { +test('component binder ignores :context/r-context during attribute fallthrough transfer', () => { const root = document.createElement('div') const comp = createComponent('
    ', { defaultName: 'PropsSkip', @@ -125,16 +125,16 @@ test('component binder ignores :props/:props-once during attribute fallthrough t ;(binder as any).__bind = (...args: any[]) => { const attr = args[2] as string - if (attr === ':props' || attr === ':props-once') return + if (attr === ':context' || attr === 'r-context') return return originalBind(...args) } root.innerHTML = - '' + '' ;(binder as any).__componentBinder.__unwrapComponents(root) const inner = root.querySelector('.inner') as HTMLElement expect(inner).toBeTruthy() - expect(inner.hasAttribute(':props')).toBe(false) - expect(inner.hasAttribute(':props-once')).toBe(false) + expect(inner.hasAttribute(':context')).toBe(false) + expect(inner.hasAttribute('r-context')).toBe(false) }) diff --git a/tests/coverage/utility-edge.spec.ts b/tests/coverage/utility-edge.spec.ts index 250fd49..b104705 100644 --- a/tests/coverage/utility-edge.spec.ts +++ b/tests/coverage/utility-edge.spec.ts @@ -15,7 +15,7 @@ import { } from '../../src/common/common' import { looseEqual } from '../../src/common/looseEqual' import { popScope, pushScope, setScope } from '../../src/composition/stack' -import { propsDirective } from '../../src/directives/props' +import { contextDirective } from '../../src/directives/context' import { ErrorType, getError } from '../../src/log/errors' import { warning, warningHandler, WarningType } from '../../src/log/warnings' import { persist } from '../../src/misc/persist' @@ -139,7 +139,7 @@ test('props directive ignores non-object payloads', () => { refs: [], context: {}, } as any - const stop = propsDirective.onBind!( + const stop = contextDirective.onBind!( document.createElement('div'), parseResult, '', @@ -204,3 +204,4 @@ test('batch catches trigger errors from observers', () => { spy.mockRestore() } }) + diff --git a/tests/directives/props-once.spec.ts b/tests/directives/props-once.spec.ts deleted file mode 100644 index a83ef56..0000000 --- a/tests/directives/props-once.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { expect, test } from 'vitest' - -import { propsOnceDirective } from '../../src/directives/props-once' -import { ref } from '../../src/reactivity/ref' - -const makeParseResult = (value: unknown, context: Record) => - ({ - value: () => [value], - stop: () => {}, - refs: [], - context, - }) as any - -test('props-once is once and collects refs', () => { - expect(propsOnceDirective.once).toBe(true) - expect(propsOnceDirective.collectRefObj).toBe(true) -}) - -test('props-once does nothing for non-object value', () => { - const ctx = { a: 1 } - const unbind = propsOnceDirective.onBind!( - document.createElement('div'), - makeParseResult(123, ctx), - 'expr', - ) - unbind() - expect(ctx.a).toBe(1) -}) - -test('props-once assigns plain values to context keys', () => { - const ctx: Record = {} - const unbind = propsOnceDirective.onBind!( - document.createElement('div'), - makeParseResult({ a: 1, b: 'x' }, ctx), - 'expr', - ) - unbind() - expect(ctx.a).toBe(1) - expect(ctx.b).toBe('x') -}) - -test('props-once writes into existing ref keys', () => { - const r = ref(0) - const ctx: Record = { a: r } - propsOnceDirective.onBind!( - document.createElement('div'), - makeParseResult({ a: 9 }, ctx), - 'expr', - ) - expect(r()).toBe(9) -}) - -test('props-once skips assignment when context value is strictly equal', () => { - const shared = { v: 1 } - let setCount = 0 - const ctx: Record = {} - Object.defineProperty(ctx, 'a', { - configurable: true, - enumerable: true, - get: () => shared, - set: () => { - setCount++ - }, - }) - - propsOnceDirective.onBind!( - document.createElement('div'), - makeParseResult({ a: shared }, ctx), - 'expr', - ) - - expect(setCount).toBe(0) -}) diff --git a/tests/directives/r-bind-mapping.spec.ts b/tests/directives/r-bind-mapping.spec.ts new file mode 100644 index 0000000..686e850 --- /dev/null +++ b/tests/directives/r-bind-mapping.spec.ts @@ -0,0 +1,91 @@ +import { expect, test } from 'vitest' + +import { createApp, html, ref } from '../../src' + +test('r-bind mapping: ":" shorthand binds as attribute', () => { + const root = document.createElement('div') + createApp( + { idValue: ref('alpha') }, + { + element: root, + template: html`
    `, + }, + ) + + const el = root.querySelector('#x') as HTMLElement | null + if (!el) throw new Error('missing #x') + expect(el.getAttribute('data-id')).toBe('alpha') +}) + +test('r-bind mapping: "." shorthand binds as property', () => { + const root = document.createElement('div') + createApp( + { titleValue: ref('hello-prop') }, + { + element: root, + template: html`
    `, + }, + ) + + const el = root.querySelector('#x') as HTMLElement | null + if (!el) throw new Error('missing #x') + expect(el.title).toBe('hello-prop') +}) + +test('r-bind mapping: ".prop" flag switches ":" binding to property binding', () => { + const root = document.createElement('div') + createApp( + { valueRef: ref('prop-value') }, + { + element: root, + template: html``, + }, + ) + + const input = root.querySelector('#i') as HTMLInputElement | null + if (!input) throw new Error('missing #i') + expect(input.value).toBe('prop-value') + // property path does not set value attribute for non-null values + expect(input.getAttribute('value')).toBeNull() +}) + +test('r-bind mapping: ".camel" camelizes option key before binding', () => { + const root = document.createElement('div') + createApp( + { v: ref('x') }, + { + element: root, + template: html`
    `, + }, + ) + + const el = root.querySelector('#x') as HTMLElement | null + if (!el) throw new Error('missing #x') + expect(el.getAttribute('data-id')).toBeNull() + // setAttribute('dataId', ...) is normalized to lowercase in HTML + expect(el.getAttribute('dataid')).toBe('x') +}) + +test('style mapping: r-style is an alias of :style', () => { + const root = document.createElement('div') + const app = createApp( + { + colorRef: ref('red'), + padRef: ref('2px'), + }, + { + element: root, + template: html`
    `, + }, + ) + + const el = root.querySelector('#x') as HTMLElement | null + if (!el) throw new Error('missing #x') + expect(el.style.color).toBe('red') + expect(el.style.padding).toBe('2px') + + app.context.colorRef('blue') + app.context.padRef('4px') + expect(el.style.color).toBe('blue') + expect(el.style.padding).toBe('4px') +}) diff --git a/tests/misc/persist.spec.ts b/tests/misc/persist.spec.ts index f1ec9a4..68e6a4e 100644 --- a/tests/misc/persist.spec.ts +++ b/tests/misc/persist.spec.ts @@ -42,3 +42,17 @@ test('should throw when key is empty', () => { getError(ErrorType.PersistRequiresKey), ) }) + +test('should return the same ref instance for inline context usage', () => { + localStorage.clear() + const source = ref(10) + const persisted = persist(source, KEY) + expect(persisted).toBe(source) + + // Can be used inline while preserving the same ref identity. + const context = { + count: persist(ref(1), 'persist-inline-count'), + } + context.count(2) + expect(localStorage.getItem('persist-inline-count')).toBe('2') +})