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 4a9db7f..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
- tag: "0.620"
+ 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 17e7087..3e584d2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,57 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
--------------------------------------------------------------------------------
+## [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
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 7708fdf..37918e8 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -1,70 +1,83 @@
-//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",
-
- // 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: "Scopes", link: "/tut/crash-course/6-scope" },
- { text: "Stateful Components", link: "/tut/crash-course/7-stateful-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: "Control Flow", link: "/tut/crash-course/11-control-flow" },
- { 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 Reactivity",
- items: [
- { text: "Nested Scopes", link: "/tut/advanced/nested-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 6481cc3..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**
- An effect is created to update the new source every frame 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 539967d..578b10f 100644
--- a/docs/api/creation.md
+++ b/docs/api/creation.md
@@ -1,39 +1,4 @@
-# Element Creation API
-
-
-
-## mount()
-
-Runs a function in a new stable scope and optionally applies its result to a
-target instance.
-
-- **Type**
-
- ```lua
- function mount(component: () -> T, target: Instance?): () -> ()
- ```
-
-- **Details**
-
- The result of the function is applied to a target in the same way
- properties are using `create()`.
-
- The function is ran in a new stable scope, just like
- [root()](reactivity-core.md#root).
-
- Returns a function that when called will destroy the stable scope.
-
-- **Example**
-
- ```lua
- local function App()
- return create "ScreenGui" {
- create "TextLabel" { Text = "Vide" }
- }
- end
-
- mount(App, game.StarterGui)
- ```
+# Element Creation
## create()
@@ -41,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**
@@ -76,73 +41,53 @@ Creates a new UI element, applying any given properties.
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)
- local con - instance:GetPropertyChangedSignal(property):Connect(function()
- callback(instance[property])
+ local cn = instance:GetPropertyChangedSignal(property):Connect(function()
+ fn(instance[property])
end)
- -- disconnect on reactive scope destruction to allow gc of instance
+ -- disconnect on scope destruction to allow gc of instance
cleanup(function()
- con:Disconnect()
+ cn:Disconnect()
end)
end)
end
@@ -150,7 +95,7 @@ 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)
}
```
@@ -161,16 +106,47 @@ A wrapper for `action()` to listen for property changes.
- **Type**
- ```lua
- function changed(property: string, callback: (...unknown) -> ()): Action
+ ```luau
+ function changed(property: string, fn: (unknown) -> ()): Action
```
- **Details**
- Will run the given callback any time the property is changed, as well as
- when the action is initially run.
+ Will run the given function immediately and whenever the property updates.
- The changed connection is disconnected when the scope the action is ran in
- is destroyed.
+ 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 861e0e1..ebbc5dd 100644
--- a/docs/api/reactivity-core.md
+++ b/docs/api/reactivity-core.md
@@ -1,36 +1,44 @@
-# Reactivity API: Core
+# Reactivity: Core
-
+## Scopes
+
+Vide code can run in one of two scopes: STABLE or REACTIVE.
+
+- 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()
+## root() STABLE
-Creates and runs a function in a new stable scope.
+Runs a function in a new stable scope.
- **Type**
- ```lua
- function root(fn: (() -> ()) -> T...): (() -> (), T...)
+ ```luau
+ function root(fn: (Destructor) -> T...): (Destructor, T...)
+
+ type Destructor = () -> ()
```
- **Details**
- Returns a function to destroy the root scope. Also passes this function as
- the first argument into its callback.
-
- All values returned by the callback are also returned following the destructor.
+ 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
+ ```luau
function source(value: T): Source
type Source =
@@ -40,83 +48,108 @@ Creates a new source with the given value.
- **Details**
- Calling the returned source with no argument will return its stored value,
- calling with an argument will set a new value.
+ 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 in a new reactive scope on source update.
+Runs a function in a new reactive scope.
- **Type**
- ```lua
- function effect(callback: () -> ())
+ ```luau
+ function effect(fn: () -> ())
```
- **Details**
- Any time a source referenced in the callback is updated, the callback will
- be reran.
-
- The callback is ran once immediately.
+ 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 in a new reactive scope 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.
-
- The callback is ran once immediately.
+ 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 6870367..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 stable scope. If it changes from a
- truthy to falsey value, the stable 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 stable 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 stable
- scope. If the input source changes, the stable scope the component was
- created in will be destroyed, and a new component created under a new
- stable 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 stable
- scope to produce a new instance.
- - For any removed index, the stable 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 stable
- scope to produce a new instance.
- - For any removed value, the stable 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 values. 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 be used to animate a change in position for the UI element.
-
---------------------------------------------------------------------------------
diff --git a/docs/api/reactivity-utility.md b/docs/api/reactivity-utility.md
index 824fe35..b8bff4d 100644
--- a/docs/api/reactivity-utility.md
+++ b/docs/api/reactivity-utility.md
@@ -1,41 +1,47 @@
-# Reactivity API: Utility
+# Reactivity: Utility
## cleanup()
-Runs a callback anytime a scope is reran or destroyed.
+Queues a callback to run when a scope is reran or destroyed.
- **Type**
- ```lua
- function cleanup(callback: () -> ())
- function cleanup(obj: Destroyable)
- function cleanup(obj: Disconnectable)
+ ```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 in a new stable scope.
+Runs a function in a new stable scope.
- **Type**
- ```lua
+ ```luau
function untrack(source: () -> T): T
```
@@ -46,7 +52,7 @@ Runs a given function in a new stable scope.
- **Example**
- ```lua
+ ```luau
local a = source(0)
local b = source(0)
@@ -55,65 +61,84 @@ Runs a given function in a new stable 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
```
## read()
-Utility used to read a value that is either a primitive or a source. Sources
-read can still be tracked inside a reactive scope.
+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 given function where any source updates made within the function do not
-trigger effects until after the function finishes running.
+Runs a function where any source updates made within the function do not
+trigger effects until after the function ends.
- **Type**
- ```lua
+ ```luau
function batch(fn: () -> ())
```
- **Details**
Improves performance when an effect depends on multiple sources, and those
- sources need to be updated. Updating those sources inside a batch call will
- only cause the effect to run once after the batch call ends instead of after
- each time a source is updated.
+ sources need to be updated.
-## context()
+- **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**
- ```lua
+ ```luau
function context(default: T): Context
type Context =
() -> T -- get
- & (T, () -> ()) -> () -- set
+ & (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 callback to set a new context with the
- given 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**
- ```lua
+ ```luau
local theme = context()
local function Button()
@@ -131,4 +156,3 @@ Creates a new context.
end)
```
---------------------------------------------------------------------------------
diff --git a/docs/api/strict-mode.md b/docs/api/strict-mode.md
index 570108b..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,25 +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 derived sources and effects twice each time they update, it helps
-ensure that derived source computations are pure, and that any
-cleanups made in derived sources or effects are done 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 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 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 don't
+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 5dce993..a329007 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -1,14 +1,12 @@
-{
- "type": "module",
-
- "scripts": {
- "docs:dev": "vitepress dev",
- "docs:build": "vitepress build",
- "docs:preview": "vitepress preview"
- },
-
- "devDependencies": {
- "vitepress": "1.0.0-rc.25",
- "vitepress-plugin-mermaid": "2.0.14"
- }
-}
+{
+ "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 @@
-