diff --git a/.gitattributes b/.gitattributes
deleted file mode 100644
index 876fbd3..0000000
--- a/.gitattributes
+++ /dev/null
@@ -1 +0,0 @@
-*.luau linguist-language=Lua
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..9c22f03
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,33 @@
+name: release
+
+on:
+ release:
+ types: [published]
+
+permissions:
+ contents: write
+
+env:
+ GH_TOKEN: ${{ github.token }}
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+
+ - name: checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: setup rokit
+ uses: CompeyDev/setup-rokit@v0.1.2
+
+ - name: build
+ run: rojo build default.project.json -o build.rbxm
+
+ - name: release
+ run: gh release upload ${{github.event.release.tag_name}} build.rbxm
+
+
+
\ No newline at end of file
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 10f047c..5bb1717 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -1,9 +1,6 @@
name: site-deploy
on:
- push:
- branches: [main] # todo: remove later
-
workflow_dispatch:
permissions:
@@ -24,21 +21,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node
- uses: actions/setup-node@v3
+ uses: actions/setup-node@v4
with:
node-version: 18
- name: Setup Pages
- uses: actions/configure-pages@v3
+ uses: actions/configure-pages@v4
- name: Install dependencies
run: npm install
- name: Build with VitePress
run: npm run docs:build
- name: Upload artifact
- uses: actions/upload-pages-artifact@v2
+ uses: actions/upload-pages-artifact@v3
with:
path: docs/.vitepress/dist
@@ -52,4 +49,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
- uses: actions/deploy-pages@v2
+ uses: actions/deploy-pages@v4
diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml
index a6cd4dc..138f29b 100644
--- a/.github/workflows/unit-test.yml
+++ b/.github/workflows/unit-test.yml
@@ -8,13 +8,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Install Luau zip
- uses: robinraju/release-downloader@v1.6
+ uses: robinraju/release-downloader@v1.9
with:
repository: Roblox/luau
- latest: true
+ tag: "0.679"
fileName: luau-ubuntu.zip
out-file-path: bin
diff --git a/.github/workflows/wallypesde.yml b/.github/workflows/wallypesde.yml
new file mode 100644
index 0000000..0c705b0
--- /dev/null
+++ b/.github/workflows/wallypesde.yml
@@ -0,0 +1,54 @@
+name: publish to wally and pesde
+
+on:
+ workflow_dispatch:
+ release:
+ types: [published]
+
+permissions:
+ contents: write
+
+env:
+ GH_TOKEN: ${{ github.token }}
+
+jobs:
+ wally:
+ runs-on: ubuntu-latest
+ steps:
+
+ - name: checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: setup rokit
+ uses: CompeyDev/setup-rokit@v0.1.2
+
+ - name: login
+ run: wally login --token "${{ secrets.WALLY_TOKEN }}"
+
+ - name: publish
+ run: wally publish
+
+ pesde:
+ runs-on: ubuntu-latest
+ steps:
+
+ - name: checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: setup rokit
+ uses: CompeyDev/setup-rokit@v0.1.2
+
+ - name: setup pesde
+ run: pesde install
+
+ - name: login
+ run: pesde auth login --token "${{ secrets.PESDE_TOKEN }}"
+
+ - name: publish
+ run: pesde publish --yes
+
+
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index bd0480d..18f0f88 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,16 @@
.vscode
_local
-aftman.toml
sourcemap.json
docs/.vitepress/dist
docs/.vitepress/cache
docs/node_modules
docs/package-lock.json
+
+luau_packages/
+lune_packages/
+.pesde/
+
+wally.lock
+pesde.lock
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2127a86..3e584d2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,9 +4,122 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
-## Unreleased
+--------------------------------------------------------------------------------
----
+## [0.4.1] - 2026-07-11
+
+### Changed
+
+- `branch()` is now allowed to be used within a reactive scope.
+
+### Deprecated
+
+- `create()` overloads. Supported is now only `create(class)(props)`.
+
+### Fixed
+
+- `create()` types in the new solver should now work without `::`.
+
+--------------------------------------------------------------------------------
+
+## [0.4.0] - 2026-01-17
+
+### Added
+
+- `create("ClassName", { props })` and `create(Instance, { props })` syntax.
+- `cleanup()` now accepts `thread` types.
+- Implicit effects to set children can now recursively create more implicit
+ effects to set children.
+- `spring()` returns a second value, a setter to set position, velocity and
+ impulse.
+- Improved `spring()` updating and unscheduling.
+- `show()` now receives a source to its callback returning the current value
+ of the condition.
+- Ignore `false` passed as a child.
+- Flag `vide.defaults` to disable the setting of default properties.
+- Delayed scope destruction for control flow functions: `show()` `switch()` `indexes()` `values()`.
+- Better `create()` types for the new type solver.
+
+### Changed
+
+- A scope can no longer be destroyed while it is active. Strict mode will check
+ for this.
+- Implicit effects to set children now unparent all children when the effect is
+ destroyed.
+- Error reporting should be improved with better formatting when effects invoke
+ other effects and no more loss of stack traces.
+- Nesting parent properties now work, and they are now also checked for
+ duplicates like other properties.
+
+### Removed
+
+- Aggregate construction when setting properties with `create()`.
+
+--------------------------------------------------------------------------------
+
+## [0.3.1] - 2024-10-09
+
+### Added
+
+- Context functions now also return results.
+- `version` table with current version.
+
+--------------------------------------------------------------------------------
+
+## [0.3.0] - 2024-10-06
+
+### Added
+
+- `context()`.
+
+### Changed
+
+- `root()` now returns its destructor as the first value by default.
+
+### Fixed
+
+- Error stack traces being lost.
+- `root()` now destroys the scope automatically if an error occurs during call.
+
+--------------------------------------------------------------------------------
+
+## [0.2.0] - 2023-11-22
+
+### Added
+
+- Batched updates with `batch()`.
+
+### Changed
+
+- Improved graph updating algorithm.
+- Graph nodes when destroyed no longer destroy children; only owned.
+
+### Fixed
+
+- Graph edge case where a destroyed node can be readded if it was queued for
+ rerun before being destroyed.
+- Some properties not being applied when `create()` is used recursively.
+
+--------------------------------------------------------------------------------
+
+## [0.1.1] - 2023-09-30
+
+### Added
+
+- `cleanup()` accepts objects with a `Destroy()` or `Disconnect()` interface.
+- `read()` as a utility to read sources or passthrough a non-source value.
+
+### Changed
+
+- Reactive scopes created within reactive scopes are now destroyed on rerun.
+- `untrack()` can be called outside of reactive scopes.
+- `changed()` will also run its callback with the initial property value.
+
+### Fixed
+
+- `show()` and `switch()` not updating when in strict mode.
+
+--------------------------------------------------------------------------------
## [0.1.0] - 2023-09-20
diff --git a/README.md b/README.md
index 8ce077b..13e41b6 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ for a quick introduction to the library.
## Code sample
-```lua
+```luau
local create = vide.create
local source = vide.source
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index 18e36cc..37918e8 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -1,67 +1,83 @@
-import { defineConfig } from "vitepress"
-
-// https://vitepress.dev/reference/site-config
-export default defineConfig({
- title: "Vide",
- titleTemplate: ":title - A reactive UI library for Luau",
- description: "A reactive UI library for Luau.",
- base: "/vide/",
- head: [["link", { rel: "icon", href: "/vide/logo.svg" }]],
-
- themeConfig: {
- logo: "/logo.svg",
-
- // https://vitepress.dev/reference/default-theme-config
- nav: [
- { text: "Home", link: "/" },
- { text: "Tutorials", link: "/tut/crash-course/1-introduction" },
- { text: "API", link: "/api/reactivity-core"},
- ],
-
- sidebar: {
- "/api/": [
- {
- text: "API",
- items: [
- { text: "Reactivity: Core", link: "/api/reactivity-core" },
- { text: "Reactivity: Utility", link: "/api/reactivity-utility" },
- { text: "Reactivity: Control Flow", link: "/api/reactivity-flow" },
- { text: "Element Creation", link: "/api/creation" },
- { text: "Animation", link: "/api/animation" },
- { text: "Strict Mode", link: "/api/strict-mode" },
- ]
- }
- ],
-
- "/tut/": [
- {
- text: "Crash Course",
- items: [
- { text: "Introduction", link: "/tut/crash-course/1-introduction" },
- { text: "Element Creation", link: "/tut/crash-course/2-creation" },
- { text: "Components", link: "/tut/crash-course/3-components" },
- { text: "Sources", link: "/tut/crash-course/4-source" },
- { text: "Effects", link: "/tut/crash-course/5-effect" },
- { text: "Stateful Components", link: "/tut/crash-course/6-stateful-component" },
- { text: "Property Binding", link: "/tut/crash-course/7-property-binding" },
- { text: "Cleanup", link: "/tut/crash-course/8-cleanup" },
- { text: "Control Flow", link: "/tut/crash-course/9-control-flow" },
- { text: "Property Nesting", link: "/tut/crash-course/10-property-nesting" },
- { text: "Actions", link: "/tut/crash-course/11-actions" },
- { text: "Strict Mode", link: "/tut/crash-course/12-strict-mode" },
- ]
- },
- {
- text: "Advanced Reactivity",
- items: [
- { text: "Reactive Scopes", link: "/tut/advanced/reactive-scoping.md"}
- ]
- }
- ],
- },
-
- socialLinks: [
- { icon: "github", link: "https://github.com/centau/vide" }
- ]
- }
-})
+//import { defineConfig } from "vitepress"
+import { withMermaid } from "vitepress-plugin-mermaid";
+
+// https://vitepress.dev/reference/site-config
+export default withMermaid({
+ title: "Vide",
+ titleTemplate: ":title - A reactive UI library for Luau",
+ description: "A reactive UI library for Luau.",
+ base: "/vide/",
+ head: [["link", { rel: "icon", href: "/vide/logo.svg" }]],
+
+ themeConfig: {
+ logo: "/logo.svg",
+
+ search: {
+ provider: "local"
+ },
+
+ footer: {
+ message: 'Released under the MIT License.',
+ },
+
+ // https://vitepress.dev/reference/default-theme-config
+ nav: [
+ { text: "Home", link: "/" },
+ { text: "Tutorials", link: "/tut/crash-course/1-introduction" },
+ { text: "API", link: "/api/reactivity-core"},
+ ],
+
+ sidebar: {
+ "/api/": [
+ {
+ text: "API",
+ items: [
+ { text: "Reactivity: Core", link: "/api/reactivity-core" },
+ { text: "Reactivity: Utility", link: "/api/reactivity-utility" },
+ { text: "Reactivity: Dynamic Scoping", link: "/api/reactivity-dynamic" },
+ { text: "Element Creation", link: "/api/creation" },
+ { text: "Animation", link: "/api/animation" },
+ { text: "Strict Mode", link: "/api/strict-mode" },
+ ]
+ }
+ ],
+
+ "/tut/": [
+ {
+ text: "Crash Course",
+ items: [
+ { text: "Introduction", link: "/tut/crash-course/1-introduction" },
+ { text: "Element Creation", link: "/tut/crash-course/2-creation" },
+ { text: "Components", link: "/tut/crash-course/3-components" },
+ { text: "Sources", link: "/tut/crash-course/4-source" },
+ { text: "Effects", link: "/tut/crash-course/5-effect" },
+ { text: "Scopes", link: "/tut/crash-course/6-scope" },
+ { text: "Reactive Components", link: "/tut/crash-course/7-reactive-component" },
+ { text: "Implicit Effects", link: "/tut/crash-course/8-implicit-effect" },
+ { text: "Derived Sources", link: "/tut/crash-course/9-derived-source" },
+ { text: "Cleanup", link: "/tut/crash-course/10-cleanup" },
+ { text: "Dynamic Scopes", link: "/tut/crash-course/11-dynamic-scopes" },
+ { text: "Actions", link: "/tut/crash-course/12-actions" },
+ { text: "Strict Mode", link: "/tut/crash-course/13-strict-mode" },
+ { text: "Concepts Summary", link: "/tut/crash-course/14-concepts" }
+ ]
+ },
+ {
+ text: "Advanced",
+ items: [
+ { text: "Dynamic Scopes", link: "/tut/advanced/dynamic-scopes"}
+ ]
+ },
+ {
+ text: "Design Patterns",
+ items: [
+ ]
+ }
+ ],
+ },
+
+ socialLinks: [
+ { icon: "github", link: "https://github.com/centau/vide" }
+ ]
+ }
+})
diff --git a/docs/.vitepress/theme/index.css b/docs/.vitepress/theme/index.css
new file mode 100644
index 0000000..9c3ca9a
--- /dev/null
+++ b/docs/.vitepress/theme/index.css
@@ -0,0 +1,25 @@
+.home > * > .VPNavBar:not(.top) {
+ backdrop-filter: blur(0.5rem);
+ background-color: transparent !important;
+}
+
+.home > * > .VPNavBar > .divider {
+ opacity: 0;
+}
+
+.home > .VPContent {
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+}
+
+.VPHome {
+ margin-top: auto !important;
+ margin-bottom: auto !important;
+}
+
+.VPBadge a {
+ text-decoration: none;
+ color: inherit
+ }
+
\ No newline at end of file
diff --git a/docs/.vitepress/theme/index.js b/docs/.vitepress/theme/index.js
index b8b9aa6..54a4c62 100644
--- a/docs/.vitepress/theme/index.js
+++ b/docs/.vitepress/theme/index.js
@@ -1,4 +1,8 @@
-// .vitepress/theme/index.js
-import DefaultTheme from 'vitepress/theme'
-import './vars.css'
-export default DefaultTheme
+// .vitepress/theme/index.js
+import DefaultTheme from 'vitepress/theme'
+import './vars.css'
+import './index.css'
+
+export default {
+ extends: DefaultTheme,
+}
diff --git a/docs/.vitepress/theme/vars.css b/docs/.vitepress/theme/vars.css
index 64df54f..7fbf3b5 100644
--- a/docs/.vitepress/theme/vars.css
+++ b/docs/.vitepress/theme/vars.css
@@ -1,3 +1,52 @@
-:root {
- --vp-c-brand-1: #3086ff;
-}
+/* Colors */
+
+:root {
+ --vp-c-brand-1: #3086ff;
+ --vp-c-brand-2: #75aeff;
+
+ --vp-button-brand-bg: #3661a2;
+ --vp-button-brand-hover-bg: #24447f;
+ --vp-button-brand-press-bg: #4896f3;
+
+ --vp-home-hero-name-color: transparent;
+ --vp-home-hero-name-background: -webkit-linear-gradient(
+ 120deg,
+ #3661a2,
+ #4896f3
+ );
+ --vp-home-hero-logo-background: -webkit-linear-gradient(
+ 120deg,
+ #3661a2,
+ #4896f3
+ );
+
+ --vp-home-hero-image-filter: blur(96px);
+
+ --vp-c-bg: #f2f5f8;
+ --vp-c-bg-alt: #dfe8f5;
+ --vp-c-bg-elv: #dde7f4;
+ --vp-c-bg-soft: #e8f1fe;
+
+ --vp-c-border: #c0c3c6;
+ --vp-c-divider: #dfe2e6;
+ --vp-c-gutter: #dfe2e6;
+ --vp-plugin-tabs-tab-bg: var(--vp-c-bg);
+
+ --vp-badge-info-bg: #122d26;
+ --vp-badge-info-text: #6bdbbd;
+ --vp-badge-tip-bg: #132741;
+ --vp-badge-tip-text: #70abfa;
+}
+
+.dark {
+ --vp-c-brand-2: #234782;
+ --vp-c-bg: #0d131b;
+ --vp-c-bg-alt: #111720;
+ --vp-c-bg-elv: #182231;
+ --vp-c-bg-soft: #182231;
+
+ --vp-c-border: #111720;
+ --vp-c-divider: #1d273c;
+ --vp-c-gutter: #181d27;
+}
+
diff --git a/docs/api/animation.md b/docs/api/animation.md
index 16e48d7..ca06b39 100644
--- a/docs/api/animation.md
+++ b/docs/api/animation.md
@@ -1,25 +1,30 @@
-# Animation API
+# Animation
-## spring()
+## spring() REACTIVE
Returns a new source with a value always moving torwards the input source value.
- **Type**
- ```lua
+ ```luau
function spring(
source: () -> T & Animatable,
period: number = 1,
damping_ratio: number = 1
- ): () -> T
+ ): (() -> T, SpringControl)
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3 | Rect
+
+ type SpringControl = ({
+ position: T?,
+ velocity: T?,
+ impulse: T?
+ }) -> ()
```
- **Details**
- The output source value is updated every step based on the input source
- value.
+ Creates a reactive scope internally to detect source updates.
The movement is physically simulated according to a
[spring](https://en.wikipedia.org/wiki/Simple_harmonic_motion).
@@ -39,3 +44,7 @@ Returns a new source with a value always moving torwards the input source value.
You can change when the solver runs by calling `vide.step(dt)`, which will
advance the simulation time by `dt` seconds and automatically stop the
solver running in heartbeat.
+
+ ::: warning
+ Large periods or damping ratios can break the spring.
+ :::
diff --git a/docs/api/creation.md b/docs/api/creation.md
index f5b4c1c..578b10f 100644
--- a/docs/api/creation.md
+++ b/docs/api/creation.md
@@ -1,38 +1,4 @@
-# Element Creation API
-
-
-
-## mount()
-
-Runs a function and applies its result to a target instance.
-
-- **Type**
-
- ```lua
- function mount(component: () -> T, target: Instance?): () -> ()
- ```
-
-- **Details**
-
- The result of the function is applies to the target in the same way
- properties are using `create()`.
-
- The function is ran in a new reactive scope, just like
- [root()](reactivity-core.md#root).
-
- Returns a function that when called will destroy the reactive scope.
-
-- **Example**
-
- ```lua
- local function App()
- return create "ScreenGui" {
- create "TextLabel" { Text = "Vide" }
- }
- end
-
- mount(App, game.StarterGui)
- ```
+# Element Creation
## create()
@@ -40,11 +6,11 @@ Creates a new UI element, applying any given properties.
- **Type**
- ```lua
+ ```luau
function create(class: string): (Properties) -> Instance
function create(instance: Instance): (Properties) -> Instance
- type Properties = Map
+ type Properties = Map
```
- **Details**
@@ -60,86 +26,68 @@ Creates a new UI element, applying any given properties.
- **Property setting rules**
- - If a table index is a string:
- - If its value is a function then it will either bind that property to
- the function or connect it if the property type is a `RBXScriptSignal`.
- - If the value is not a function then the property will be set to that
- value.
- - If a table index is a number:
- - If its value is an action then that action will be queued to run after
- properties are set.
- - If its value is a table then that table will be recursively
- processed just like the outer table.
- - If its value is a function then it will bind the instances children to
- that function.
- - If its value is an instance then it will be parented to the instance.
+ - **index is string:**
+ - **value is function:**
+ - **property is event:** connect function as callback
+ - **property is not event:** create effect to update property
+ - **value is not function:** set property to value
+ - **index is number:**
+ - **value is action:** run action
+ - **value is table:** recurse table
+ - **value is function:** create effect to update children
+ - **value is instance:** set instance as child
- **Example**
Basic element creation.
- ```lua
- local frame = create "Frame" {
- Name = "NewFrame",
- Position = UDim2.fromScale(1, 0)
+ ```luau
+ local frame = create "TextButton" {
+ Name = "Button",
+ Size = UDim2.fromOffset(200, 160),
+
+ Activated = function()
+ print "clicked"
+ end,
+
+ create "UICorner" {}
}
```
- A component using property nesting.
-
- ```lua
- type Layout = {
- Layout = {
- Position: UDim2?,
- Size: UDim2?,
- AnchorPoint: Vector2?
- }
- }
-
- type Children = {
- Children = Array
- }
-
- function Background(props: Layout & Children & {
- Color: Color3
- })
- return create "Frame" {
- BackgroundColor3 = props.Color,
- props.Layout,
- props.Children
- }
- end
- ```
-
## action()
-Creates a callback that can be passed to `create()` to invoke custom actions on
-instances.
+Creates a special object that can be passed to `create()` to invoke custom
+actions on instances.
- **Type**
- ```lua
+ ```luau
function action((Instance) -> (), priority: number = 1): Action
```
- **Details**
- When passed to `create()`, the given callback is called with the instance
- being created as the only argument. Actions take precedence over property
- and child assignments.
+ When passed to `create()`, the function is called with the instance being
+ created as the only argument. Actions take precedence over property and
+ child assignments.
A priority can be optionally specified to ensure certain actions run after
- other actions. Higher priority numbers are ran after lower priority numbers.
+ other actions. Lower priority values are ran first.
- **Example**
An action to listen to changed properties:
- ```lua
- local function changed(property: string, callback: (new) -> ())
+ ```luau
+ local function changed(property: string, fn: (new) -> ())
return action(function(instance)
- instance:GetPropertyChangedSignal("property"):Connect(function()
- callback(instance[property])
+ local cn = instance:GetPropertyChangedSignal(property):Connect(function()
+ fn(instance[property])
+ end)
+
+ -- disconnect on scope destruction to allow gc of instance
+ cleanup(function()
+ cn:Disconnect()
end)
end)
end
@@ -147,7 +95,58 @@ instances.
local output = source ""
create "TextBox" {
- -- will update the `output` source anytime the text property is changed
+ -- will update the output source anytime the text property is changed
changed("Text", output)
}
```
+
+## changed()
+
+A wrapper for `action()` to listen for property changes.
+
+- **Type**
+
+ ```luau
+ function changed(property: string, fn: (unknown) -> ()): Action
+ ```
+
+- **Details**
+
+ Will run the given function immediately and whenever the property updates.
+
+ The function is called with the updated property value.
+
+ Runs with an action priority of 1.
+
+## mount() STABLE
+
+Runs a function in a new stable scope and optionally applies its result to a
+target instance.
+
+- **Type**
+
+ ```luau
+ function mount(component: () -> T, target: Instance?): () -> ()
+ ```
+
+- **Details**
+
+ This is a utility for `root()` when parenting a component to an existing
+ instance.
+
+ The result of the function is applied to a target in the same way
+ properties are using `create()`.
+
+ Returns a function that when called will destroy the stable scope.
+
+- **Example**
+
+ ```luau
+ local function App()
+ return create "ScreenGui" {
+ create "TextLabel" { Text = "Vide" }
+ }
+ end
+
+ local destroy = mount(App, game.StarterGui)
+ ```
diff --git a/docs/api/reactivity-core.md b/docs/api/reactivity-core.md
index 0e4a5d0..ebbc5dd 100644
--- a/docs/api/reactivity-core.md
+++ b/docs/api/reactivity-core.md
@@ -1,132 +1,155 @@
-# Reactivity API: Core
+# Reactivity: Core
-
+## Scopes
-## root()
+Vide code can run in one of two scopes: STABLE or REACTIVE.
-Creates and runs a function in a new reactive scope.
+- Reactive scopes rerun if a source read within updates.
+- Stable scopes never rerun.
+- Reactive scopes cannot be created directly within another reactive scope.
+- When a scope is destroyed, all scopes created within are also destroyed.
+
+Different functions in Vide's API will run code in different scopes.
+
+:::warning
+Yielding is not allowed in any stable or reactive scope. Strict mode will check
+for this.
+:::
+
+## root() STABLE
+
+Runs a function in a new stable scope.
- **Type**
- ```lua
- function root(fn: (destroy: () -> ()) -> T...): T...
+ ```luau
+ function root(fn: (Destructor) -> T...): (Destructor, T...)
+
+ type Destructor = () -> ()
```
- **Details**
- Creates a new root reactive scope, where creation and derivations of sources
- can be tracked and properly disposed of.
-
- Returns the result of the given function.
-
- A function to destroy the root is passed into the callback, which will run
- any cleanups and allow derived sources created to garbage collect.
-
- ::: warning
- `fn()` cannot yield.
- :::
+ Returns a destructor and any values returned by the callback.
## source()
-Creates a new source with the given value.
+Creates a new source.
- **Type**
- ```lua
- function source(value: T): (T?) -> T
+ ```luau
+ function source(value: T): Source
+
+ type Source =
+ () -> T -- get
+ & (T) -> () -- set
```
- **Details**
- Calling the returned source with no argument will return its stored value,
- calling with an argument will set a new value.
-
- Reading from the source from within a reactive scope will cause changes
- to that source to be tracked and anything depending on it to update.
+ Call the returned source with no argument to read its value.
+ Call the returned source with an argument to set its value.
- **Example**
- ```lua
+ ```luau
local count = source(0)
-
- count() -- 0
-
- count(count() + 1) -- 1
+ print(count())-- 0
+ count(count() + 1)
+ print(count()) -- 1
```
-## effect()
+## effect() REACTIVE
-Runs a side-effect on source update.
+Runs a function in a new reactive scope.
- **Type**
- ```lua
- function effect(callback: () -> ())
+ ```luau
+ function effect(fn: () -> ())
```
- **Details**
- The callback is ran immediately.
-
- Any time a source referenced in the callback is changed, the callback will
- be reran.
-
- ::: warning
- `callback()` cannot yield.
- :::
+ The function is ran once immediately.
- **Example**
- ```lua
- local num = source(1)
+ ```luau
+ local count = source(1)
effect(function()
- print(num())
+ print(count())
end)
-- prints 1
- num(num() + 1)
+ count(2)
-- prints 2
```
-## derive()
+## derive() REACTIVE
-Derives a new source from existing sources.
+Runs a function in a new reactive scope to compute a value for new source.
- **Type**
- ```lua
- function derive(source: () -> T): () -> T
+ ```luau
+ function derive(fn: () -> T): () -> T
```
- **Details**
- The derived source will have its value recalculated when any source source
- it derives from is updated.
+ Anytime the reactive scope reruns, the output source value is set to what is
+ returned.
- Anytime its value is recalculated it is also cached, subsequent calls will
- retun this cached value until it recalculates again.
-
- Takes a callback that is immediately run to determine what sources are being
- referenced.
-
- ::: warning
- `source()` cannot yield.
- :::
+ The function is ran once immediately.
- **Example**
- ```lua
+ ```luau
local count = source(0)
local text = derive(function() return `count: {count()}` end)
- text() -- "count: 0"
+ print(text()) -- "count: 0"
count(1)
- text() -- "count: 1"
+ print(text()) -- "count: 1"
```
---------------------------------------------------------------------------------
+ A `derive()` should be used instead of a pure function when you expect it to
+ be read multiple times between updates, because `derive()` will cache the
+ result to prevent recomputing it on every read.
+
+ ::: code-group
+
+ ```luau [Pure Function]
+ local count = source(0)
+
+ local text = function()
+ print "ran"
+ return `count: {count()}`
+ end
+
+ count(1)
+ print(text()) -- prints "ran" followed by "count: 1"
+ print(text()) -- prints "ran" followed by "count: 1"
+ ```
+
+ ```luau [Derived Source]
+ local count = source(0)
+
+ local text = derive(function() -- [!code highlight]
+ print "ran"
+ return `count: {count()}`
+ end) -- [!code highlight]
+
+ count(1) -- prints "ran"
+ print(text()) -- prints "count: 1"
+ print(text()) -- prints "count: 1"
+ ```
+
+ :::
diff --git a/docs/api/reactivity-dynamic.md b/docs/api/reactivity-dynamic.md
new file mode 100644
index 0000000..f0b3229
--- /dev/null
+++ b/docs/api/reactivity-dynamic.md
@@ -0,0 +1,229 @@
+# Reactivity: Dynamic Scopes
+
+Dynamic scopes are scopes that are created or destroyed in response to
+source updates. Vide provides functions for some common use-cases for dynamic scopes.
+
+## show() REACTIVE
+
+Shows a component if the source is truthy. Optionally shows a fallback component
+if the source is falsey.
+
+- **Type**
+
+ ```luau
+ function show(source: () -> unknown, component: Constructor): () -> T?
+ function show(source: () -> unknown, component: Constructor, fallback: () -> U): () -> T | U
+
+ type Constructor = () -> (T, number?)
+ ```
+
+- **Details**
+
+ Creates a reactive scope internally to detect source updates.
+
+ The component is run in a stable scope when truthy, otherwise the stable
+ scope is destroyed.
+
+ Returns a source holding an instance of the currently shown component or
+ `nil` if no component is currently shown.
+
+ Destruction of the scope can be delayed by returning the number of seconds
+ to delay by, after the component.
+
+## switch() REACTIVE
+
+Shows one of a set of components depending on a source and a mapping table.
+
+- **Type**
+
+ ```luau
+ function switch(source: () -> K): (map: Map>): () -> V?
+
+ type Constructor = () -> (T, number?)
+ ```
+
+- **Details**
+
+ Creates a reactive scope internally to detect source updates.
+
+ When the source updates, its value is inputted into a map to get a component
+ constructor. This component is then run in a stable scope. The previous
+ stable scope is destroyed.
+
+ Returns a source holding an instance of the currently shown component or
+ `nil` if no component is currently shown.
+
+ Destruction of the scope can be delayed by returning the number of seconds
+ to delay by, after the component.
+
+- **Example**
+
+ ```luau
+ local logged = source(false)
+
+ local button = switch(logged) {
+ [true] = function()
+ return Button { Text = "Log out", Toggle = logged }
+ end,
+
+ [false] = function()
+ return Button { Text = "Log in", Toggle = logged }
+ end
+ }
+ ```
+
+## indexes() REACTIVE
+
+Shows a component for each index in a table.
+
+- **Type**
+
+ ```luau
+ function indexes(
+ source: () -> Map,
+ constructor: (value: () -> VI, index: KI) -> (VO, number?)
+ ): Array
+ ```
+
+- **Details**
+
+ Creates a reactive scope internally to detect source updates.
+
+ When the source table updates, a component is generated for each index in
+ the table.
+
+ - For any added index, the `constructor` function is run in a new stable
+ scope to produce an instance that is cached.
+ - For any removed index, the stable scope for that index is destroyed.
+
+ The `constructor` function is called with:
+
+ 1. A *source containing the index's value*.
+ 2. The *index itself*.
+
+ Anytime an existing index's value changes, the `constructor` function is not
+ rerun, instead, that index's corresponding source is updated with the new
+ value.
+
+ Returns a source holding an array of instances currently shown.
+
+ Destruction of the scope can be delayed by returning the number of seconds
+ to delay by, after the component.
+
+- **Example**
+
+ ```luau
+ type Item = {
+ name: string,
+ icon: number
+ }
+
+ local items = source {} :: () -> Array
+
+ local displays = indexes(items, function(item, i)
+ return ItemDisplay {
+ Name = function()
+ return i .. ": " .. item().name
+ end,
+
+ Image = function()
+ return "rbxassetid://" .. item().icon
+ end,
+ }
+ end)
+ ```
+
+## values() REACTIVE
+
+Shows a component for each value in a table.
+
+- **Type**
+
+ ```luau
+ function values(
+ source: () -> Map,
+ constructor: (value: VI, index: () -> KI) -> (VO, number?)
+ ): Array
+
+- **Details**
+
+ Operates with the same idea as `indexes()`, but applied to values instead of
+ indexes.
+
+ Creates a reactive scope internally to detect source updates.
+
+ When the source table updates, a component is generated for each value in
+ the table.
+
+ - For any added value, the `constructor` function is run in a new stable scope
+ to produce an instance that is cached.
+ - For any removed value, the stable scope for that value is destroyed.
+
+ The `constructor` function is called with:
+
+ 1. The *value itself*.
+ 2. A *source containing the value's index*.
+
+ Anytime an existing value's index changes, the `constructor` function is not
+ rerun, instead, that value's corresponding source is updated with the new
+ index.
+
+ Returns a source holding an array of instances currently shown.
+
+ Destruction of the scope can be delayed by returning the number of seconds
+ to delay by, after the component.
+
+ ::: warning
+ Having the same values appear multiple times in the input source table can
+ cause unexpected behavior. Strict mode has checks for this.
+ :::
+
+- **Example**
+
+ ```luau
+ type Item = {
+ name: string,
+ icon: number
+ }
+
+ local items = source {} :: () -> Array
+
+ local displays = values(items, function(item, i)
+ return ItemDisplay {
+ Name = function()
+ return i() .. ": " .. item.Name
+ end
+
+ Image = "rbxassetid://" .. item.icon,
+ }
+ end)
+ ```
+
+- **Extra**
+
+ When should you use `indexes()` and `values()`?
+
+ `values()` should be used when you have a fixed set of objects where the
+ same objects can be re-arranged in the source table. It maps a value to a
+ UI element.
+
+ e.g.
+ - List of all players.
+ - Inventory of items.
+ - Chat message history.
+ - Toast notifications.
+
+ `indexes()` should be used in other cases, especially when your source table
+ has primitive values. It maps an index to a UI element.
+
+ e.g.
+ - List of character or weapon stats.
+
+ In most cases, both functions will produce the same observed result.
+ The main difference is performance, picking the right function to use can
+ result in less property updates and less re-renders. One case to note is
+ that `values()` works nicely when animating re-ordering of instances, since
+ the source index can be used to animate a change in position for the UI
+ element.
+
+--------------------------------------------------------------------------------
diff --git a/docs/api/reactivity-flow.md b/docs/api/reactivity-flow.md
deleted file mode 100644
index 47dafa8..0000000
--- a/docs/api/reactivity-flow.md
+++ /dev/null
@@ -1,218 +0,0 @@
-# Reactivity API: Control Flow
-
-
-
-## show()
-
-Shows one of two components depending on an input source.
-
-- **Type**
-
- ```lua
- function show(source: () -> unknown, component: () -> T): () -> T?
- function show(source: () -> unknown, component: () -> T, fallback: () -> U): () -> T | U
- ```
-
-- **Details**
-
- Returns a source holding an instance of the currently shown component.
-
- When the input source changes from a falsey to a truthy value, the
- component will be reran under a new reactive scope. If it changes from a
- truthy to falsey value, the reactive scope the component was created in will
- be destroyed, and the returned source will output `nil`, or a fallback
- component if given.
-
- The fallback component is also ran under a new reactive scope, and destroyed
- when the input source switches back to truthy.
-
-## switch()
-
-Shows one of a set of components depending on an input source and a mapping table.
-
-- **Type**
-
- ```lua
- function switch(source: () -> K): (map: Map V>) -> V?
- ```
-
-- **Details**
-
- Returns a source holding an instance of the currently shown component.
-
- When the input source changes, the new value will be used to lookup a given
- mapping table to get a component, which will be ran under a new reactive
- scope. If the input source changes, the reactive scope the component was
- created in will be destroyed, and a new component created under a new
- reactive scope. If no component is found for an input value, the switch will
- output `nil`.
-
-- **Example**
-
- ```lua
- local logged = source(false)
-
- local button = switch(logged) {
- [true] = function()
- return Button { Text = "Log out", Toggle = logged }
- end,
-
- [false] = function()
- return Button { Text = "Log in", Toggle = logged }
- end
- }
- ```
-
-## indexes()
-
-Maps each index in a table source to an object.
-
-- **Type**
-
- ```lua
- function indexes(
- source: () -> Map,
- transform: (value: () -> VI, index: KI) -> VO
- ): Array
-
-- **Details**
-
- Returns a source holding an array of instances currently shown.
-
- When the input source changes, each *index* in the new table is compared with
- the last input table.
-
- - For any new index, the `transform` function is ran under a new reactive
- scope to produce a new instance.
- - For any removed index, the reactive scope for that index is destroyed.
- - Unchanged indexes are untouched.
-
- The transform function is called only ever *once* for each index in the
- source table.
-
- 1. First argument is a *source containing the index's value*.
- 2. Second argument is the *index itself*.
-
- Anytime an existing index's value changes, the transform function is not
- rerun, instead the source value for that index will update, causing anything
- depending on it to update too.
-
-- **Example**
-
- The intended purpose of this function is to map each index in a table to
- a UI element.
-
- ```lua
- type Item = {
- name: string,
- icon: number
- }
-
- local items = source {} :: () -> Array
-
- local displays = indexes(items, function(item, i)
- return ItemDisplay {
- Name = function()
- return i .. ": " .. item().name
- end,
-
- Image = function()
- return "rbxassetid://" .. item().icon
- end,
- }
- end)
- ```
-
-## values()
-
-Maps each value in a table source to an object.
-
-- **Type**
-
- ```lua
- function values(
- source: () -> Map,
- transform: (value: VI, index: () -> KI) -> VO
- ): Array
-
-- **Details**
-
- Returns a source holding an array of instances currently shown.
-
- When the input source changes, each *value* in the new table is compared with
- the last input table. Similar to `indexes()` but for values instead of indexes.
-
- - For any new value, the `transform` function is ran under a new reactive
- scope to produce a new instance.
- - For any removed value, the reactive scope for that value is destroyed.
- - Unchanged values are untouched.
-
- The transform function is only ever called *once* for each value in the
- source table.
-
- 1. First argument is the *value itself*.
- 2. Second argument is a *source containing the value's index*.
-
- Anytime an existing value's index changes, the transform function is not
- rerun, instead the source index for that value will update, causing anything
- depending on it to update too.
-
- ::: warning
- Having primitive values in the input source table can cause unexpected
- behavior, as duplicate values can result in multiple tranforms being ran for
- a single value, meaning there can be multiple source indexes bound to the
- same UI element. Strict mode has checks for this.
- :::
-
-- **Example**
-
- The intended purpose of this function is to map each value in a table to
- a UI element.
-
- ```lua
- type Item = {
- name: string,
- icon: number
- }
-
- local items = source {} :: () -> Array
-
- local displays = values(items, function(item, i)
- return ItemDisplay {
- Name = function()
- return i() .. ": " .. item.Name
- end
-
- Image = "rbxassetid://" .. item.icon,
- }
- end)
- ```
-
-- **Extra**
-
- When should you use `indexes()` and `values()`?
-
- `values()` should be used when you have a fixed set of objects where the
- same objects can be re-arranged in the source table. It maps a value to a
- UI element.
-
- e.g.
- - List of all players.
- - Inventory of items.
- - Chat message history.
- - Toast notifications.
-
- `indexes()` should be used in other cases, especially when your source table
- has primitive value. It maps an index to a UI element.
-
- e.g.
- - List of character or weapon stats.
-
- In most cases, both functions will appear to have the same behavior.
- The main difference is performance, picking the right function to use can
- result in less property updates and less re-renders. One case to note is
- that `values()` works nicely when animating re-ordering of instances, since
- the value is not destroyed when indexes are changed, and the source index
- can easily be put through a spring.
-
---------------------------------------------------------------------------------
diff --git a/docs/api/reactivity-utility.md b/docs/api/reactivity-utility.md
index 9219b29..b8bff4d 100644
--- a/docs/api/reactivity-utility.md
+++ b/docs/api/reactivity-utility.md
@@ -1,47 +1,58 @@
-# Reactivity API: Utility
+# Reactivity: Utility
## cleanup()
-Runs a callback anytime a reactive scope is re-ran.
+Queues a callback to run when a scope is reran or destroyed.
- **Type**
- ```lua
- function cleanup(callback: () -> ())
+ ```luau
+ function cleanup(v: Function | Disconnectable | Destroyable | thread)
+
+ type Function = () -> ()
+ type Destroyable = { destroy: () -> () }
+ type Disconnectable = { disconnect: () -> () }
```
- **Example**
- ```lua
- local data = source(1)
+ ```luau
+ local count = source(0)
- effect(function()
- local label = create "TextLabel" { Text = data() }
+ local destroy = root(function()
+ effect(function()
+ count()
- cleanup(function()
- label:Destroy()
+ cleanup(function()
+ print "cleaned"
+ end)
end)
- end)
+ end
+
+ -- nothing printed yet
+ count(1) -- prints "cleaned"
+ count(2) -- prints "cleaned"
+ destroy() -- prints "cleaned"
```
-## untrack()
+## untrack() STABLE
-Runs a given function where any sources read will not track its reactive scope.
+Runs a function in a new stable scope.
- **Type**
- ```lua
+ ```luau
function untrack(source: () -> T): T
```
- **Details**
- Updates made to a source passed to `untrack()` will not cause updates to
- anything depending on that source.
+ Can be used inside a reactive scope to read from sources you do not want
+ tracked by the reactive scope.
- **Example**
- ```lua
+ ```luau
local a = source(0)
local b = source(0)
@@ -50,9 +61,9 @@ Runs a given function where any sources read will not track its reactive scope.
end)
print(sum()) -- 0
- b(1)
+ b(1) -- untracked so reactive scope created by derive() does not rerun
print(sum()) -- 0
- a(1)
+ a(1) -- reactive scope created by derive() reruns
print(sum()) -- 2
```
@@ -62,8 +73,86 @@ Utility used to read a value that is either a primitive or a source.
- **Type**
- ```lua
+ ```luau
function read(value: T | () -> T): T
```
---------------------------------------------------------------------------------
+## batch()
+
+Runs a function where any source updates made within the function do not
+trigger effects until after the function ends.
+
+- **Type**
+
+ ```luau
+ function batch(fn: () -> ())
+ ```
+
+- **Details**
+
+ Improves performance when an effect depends on multiple sources, and those
+ sources need to be updated.
+
+- **Example**
+
+ ```luau
+ local a = source(0)
+ local b = source(0)
+
+ effect(function()
+ print(a() + b())
+ end)
+
+ -- prints "0"
+
+ batch(function()
+ a(1) -- no print
+ b(2) -- no print
+ end)
+
+ -- prints "3"
+ ```
+
+## context() STABLE
+
+Creates a new context.
+
+- **Type**
+
+ ```luau
+ function context(default: T): Context
+
+ type Context =
+ () -> T -- get
+ & (T, () -> U) -> U -- set
+ ```
+
+- **Details**
+
+ Calling `context()` returns a new context function.
+ Call this function with no arguments to get the context value.
+ Call this function with a value and a function to create a new context with
+ the given value.
+
+ The new context is run under a stable scope.
+
+- **Example**
+
+ ```luau
+ local theme = context()
+
+ local function Button()
+ print(theme())
+ end
+
+ root(function()
+ theme("light", function()
+ Button() -- prints "light"
+
+ theme("dark", function()
+ Button() -- prints "dark"
+ end)
+ end)
+ end)
+ ```
+
diff --git a/docs/api/strict-mode.md b/docs/api/strict-mode.md
index 35de4d8..6f9ee80 100644
--- a/docs/api/strict-mode.md
+++ b/docs/api/strict-mode.md
@@ -2,7 +2,7 @@
Strict mode is library-wide and can get set by doing:
-```lua
+```luau
vide.strict = true
```
@@ -14,24 +14,24 @@ and identifying improper usage.
Currently, strict mode will:
-1. Run derived sources twice a source updates.
-2. Run effects twice when a source updates.
-3. Throw an error if yields occur where they are not allowed.
-4. Checks for `indexes()` and `values()` returning primitive values.
-5. Checks for `values()` input having duplicate values.
-6. Checks for duplicate nested properties at same depth.
-7. Better error reporting and stack traces + creation traces of property bindings.
+1. Run reactive scopes twice when a source updates.
+2. Throw an error if yields occur where they are not allowed.
+3. Checks for `indexes()` and `values()` outputting primitive values.
+4. Checks for `values()` input having duplicate values.
+5. Checks for duplicate nested properties at same depth.
+6. Checks for destruction of an active scope.
+7. Better error reporting and stack traces.
-By rerunning sources and effects, any side-effects are made more apparent.
-This also helps ensure that cleanups are being handled correctly.
+By rerunning reactive scopes twice each time they update, it helps ensure that
+computations are pure, and that any cleanup is done correctly.
Accidental yielding within reactive scopes can break Vide's reactive graph,
-which strict mode can catch.
+which strict mode will catch.
As well as additional safety checks, Vide will dedicate extra resources to
recording and better emitting stack traces where errors occur, particularly
-when binding properties to sources.
+when implicit effects are created for instance property updating.
-It is recommend to develop UI with strict mode and to disable it when pushing to
-production. In Roblox, production code compiles at O2 by default, so you don't
+It is recommended to develop UI with strict mode and to disable it when pushing to
+production. In Roblox, production code compiles at O2 by default, so you do not
need to worry about disabling strict mode unless you have manually enabled it.
diff --git a/docs/index.md b/docs/index.md
index 6fe3a4a..295d137 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,14 +1,16 @@
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
+pageClass: home
+next:
+ text: 'Introduction'
+ link: '/tut/crash-course/1-introduction'
hero:
- name: Vide
- text: ""
+ name: "Vide"
tagline: A reactive UI library for Luau.
image:
src: /logo.svg
- alt: Vide
actions:
- theme: brand
text: Tutorials
@@ -17,8 +19,4 @@ hero:
text: API Reference
link: /api/reactivity-core
-features:
- - title: In Development
- details: Not recommended for production use.
---
-
diff --git a/docs/package.json b/docs/package.json
index 921ee39..a329007 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -1,13 +1,12 @@
-{
- "type": "module",
-
- "scripts": {
- "docs:dev": "vitepress dev",
- "docs:build": "vitepress build",
- "docs:preview": "vitepress preview"
- },
-
- "devDependencies": {
- "vitepress": "^1.0.0-rc.4"
- }
-}
+{
+ "type": "module",
+ "scripts": {
+ "docs:dev": "vitepress dev",
+ "docs:build": "vitepress build",
+ "docs:preview": "vitepress preview"
+ },
+ "devDependencies": {
+ "vitepress": "^1.4.5",
+ "vitepress-plugin-mermaid": "2.0.17"
+ }
+}
diff --git a/docs/public/full_logo.svg b/docs/public/full_logo.svg
index 85488e0..1962800 100644
--- a/docs/public/full_logo.svg
+++ b/docs/public/full_logo.svg
@@ -1,4 +1,4 @@
-