From a3cc2dfbdaa1e19fea1e363f7be01a32423678b9 Mon Sep 17 00:00:00 2001 From: aaron <83140718+centau@users.noreply.github.com> Date: Mon, 4 Nov 2024 23:18:04 +0000 Subject: [PATCH] Update docs --- docs/.vitepress/config.ts | 15 +- docs/.vitepress/theme/{home.css => index.css} | 8 +- docs/.vitepress/theme/index.js | 4 +- docs/.vitepress/theme/vars.css | 8 +- docs/api/animation.md | 11 +- docs/api/creation.md | 142 +++++------- docs/api/reactivity-core.md | 109 ++++++--- docs/api/reactivity-dynamic.md | 212 +++++++++++++++++ docs/api/reactivity-flow.md | 218 ------------------ docs/api/reactivity-utility.md | 78 ++++--- docs/api/strict-mode.md | 22 +- docs/tut/advanced/nested-scoping.md | 192 --------------- docs/tut/control-flow/1-intro.md | 0 docs/tut/control-flow/2-show.md | 1 - docs/tut/control-flow/3-switch.md | 1 - docs/tut/control-flow/4-indexes.md | 1 - docs/tut/control-flow/5-values.md | 1 - docs/tut/crash-course/1-introduction.md | 16 +- docs/tut/crash-course/10-cleanup.md | 2 +- docs/tut/crash-course/11-control-flow.md | 98 -------- docs/tut/crash-course/11-dynamic-scope.md | 158 +++++++++++++ docs/tut/crash-course/12-actions.md | 7 +- docs/tut/crash-course/13-strict-mode.md | 1 + docs/tut/crash-course/14-concepts.md | 34 +-- docs/tut/crash-course/5-effect.md | 26 ++- docs/tut/crash-course/6-scope.md | 58 ++--- ...l-component.md => 7-reactive-component.md} | 8 +- docs/tut/crash-course/8-implicit-effect.md | 32 ++- docs/tut/crash-course/9-derived-source.md | 13 +- docs/tut/dynamic-scoping/custom.md | 144 ++++++++++++ 30 files changed, 850 insertions(+), 770 deletions(-) rename docs/.vitepress/theme/{home.css => index.css} (83%) create mode 100644 docs/api/reactivity-dynamic.md delete mode 100644 docs/api/reactivity-flow.md delete mode 100644 docs/tut/advanced/nested-scoping.md delete mode 100644 docs/tut/control-flow/1-intro.md delete mode 100644 docs/tut/control-flow/2-show.md delete mode 100644 docs/tut/control-flow/3-switch.md delete mode 100644 docs/tut/control-flow/4-indexes.md delete mode 100644 docs/tut/control-flow/5-values.md delete mode 100644 docs/tut/crash-course/11-control-flow.md create mode 100644 docs/tut/crash-course/11-dynamic-scope.md rename docs/tut/crash-course/{7-stateful-component.md => 7-reactive-component.md} (91%) create mode 100644 docs/tut/dynamic-scoping/custom.md diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 1b30a51..380542a 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -34,7 +34,7 @@ export default withMermaid({ items: [ { text: "Reactivity: Core", link: "/api/reactivity-core" }, { text: "Reactivity: Utility", link: "/api/reactivity-utility" }, - { text: "Reactivity: Control Flow", link: "/api/reactivity-flow" }, + { 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" }, @@ -52,20 +52,25 @@ export default withMermaid({ { 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: "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: "Control Flow", link: "/tut/crash-course/11-control-flow" }, + { text: "Dynamic Scoping", link: "/tut/crash-course/11-dynamic-scope" }, { 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", + text: "Dynamic Scoping", + items: [ + { text: "Custom Scopes", link: "/tut/dynamic-scoping/custom"} + ] + }, + { + text: "Design Patterns", items: [ - { text: "Nested Scopes", link: "/tut/advanced/nested-scoping.md"} ] } ], diff --git a/docs/.vitepress/theme/home.css b/docs/.vitepress/theme/index.css similarity index 83% rename from docs/.vitepress/theme/home.css rename to docs/.vitepress/theme/index.css index c5f042c..9c3ca9a 100644 --- a/docs/.vitepress/theme/home.css +++ b/docs/.vitepress/theme/index.css @@ -16,4 +16,10 @@ .VPHome { margin-top: auto !important; margin-bottom: auto !important; -} \ No newline at end of file +} + +.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 7f7e9a7..54a4c62 100644 --- a/docs/.vitepress/theme/index.js +++ b/docs/.vitepress/theme/index.js @@ -1,8 +1,8 @@ // .vitepress/theme/index.js import DefaultTheme from 'vitepress/theme' import './vars.css' -import './home.css' +import './index.css' export default { extends: DefaultTheme, -} \ No newline at end of file +} diff --git a/docs/.vitepress/theme/vars.css b/docs/.vitepress/theme/vars.css index 6556617..7fbf3b5 100644 --- a/docs/.vitepress/theme/vars.css +++ b/docs/.vitepress/theme/vars.css @@ -31,6 +31,11 @@ --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 { @@ -43,4 +48,5 @@ --vp-c-border: #111720; --vp-c-divider: #1d273c; --vp-c-gutter: #181d27; -} \ No newline at end of file +} + diff --git a/docs/api/animation.md b/docs/api/animation.md index 9f3b805..8b84ce9 100644 --- a/docs/api/animation.md +++ b/docs/api/animation.md @@ -1,6 +1,6 @@ -# Animation API +# Animation -## spring() +## spring() REACTIVE Returns a new source with a value always moving torwards the input source value. @@ -18,8 +18,7 @@ Returns a new source with a value always moving torwards the input source value. - **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 +38,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 528b4fb..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** - - ```luau - 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** - - ```luau - local function App() - return create "ScreenGui" { - create "TextLabel" { Text = "Vide" } - } - end - - mount(App, game.StarterGui) - ``` +# Element Creation ## create() @@ -45,7 +10,7 @@ Creates a new UI element, applying any given properties. function create(class: string): (Properties) -> Instance function create(instance: Instance): (Properties) -> Instance - type Properties = Map + type Properties = Map ``` - **Details** @@ -77,42 +42,22 @@ Creates a new UI element, applying any given properties. Basic element creation. ```luau - local frame = create "Frame" { - Name = "NewFrame", - Position = UDim2.fromScale(1, 0) + local frame = create "TextButton" { + Name = "Button", + Size = UDim2.fromOffset(200, 160), + + Activated = function() + print "clicked" + end, + + create "UICorner" {} } ``` - A component using property nesting. - - ```luau - 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** @@ -122,27 +67,27 @@ instances. - **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: ```luau - local function changed(property: string, callback: (new) -> ()) + 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) } ``` @@ -162,15 +107,46 @@ A wrapper for `action()` to listen for property changes. - **Type** ```luau - function changed(property: string, callback: (...unknown) -> ()): Action + 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 b68911c..ebbc5dd 100644 --- a/docs/api/reactivity-core.md +++ b/docs/api/reactivity-core.md @@ -1,32 +1,40 @@ -# 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** ```luau - function root(fn: (() -> ()) -> T...): (() -> (), T...) + 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** @@ -40,71 +48,64 @@ 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** ```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** ```luau - function effect(callback: () -> ()) + 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** ```luau - local num = source(1) + 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** ```luau - function derive(source: () -> T): () -> T + 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** @@ -112,11 +113,43 @@ Derives a new source in a new reactive scope from existing sources. 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..a3e5168 --- /dev/null +++ b/docs/api/reactivity-dynamic.md @@ -0,0 +1,212 @@ +# Reactivity: Dynamic Scoping + +Dynamic scoping is the act of creating and destroying new scopes in response to +source updates. Vide provides functions for some common use-cases to do this. + +## 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: () -> T): () -> T? + function show(source: () -> unknown, component: () -> T, fallback: () -> U): () -> T | U + ``` + +- **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. + +## 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>) () -> V? + ``` + +- **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. + +- **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, + transform: (value: () -> VI, index: KI) -> VO + ): 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 `transform` 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 `transform` function is called with: + + 1. A *source containing the index's value*. + 2. The *index itself*. + + Anytime an existing index's value changes, the `transform` 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. + +- **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, + transform: (value: VI, index: () -> KI) -> VO + ): 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 `transform` 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 `transform` function is called with: + + 1. The *value itself*. + 2. A *source containing the value's index*. + + Anytime an existing value's index changes, the `transform` 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. + + ::: 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 03aea97..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** - - ```luau - 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** - - ```luau - 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** - - ```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() - -Maps each index in a table source to an object. - -- **Type** - - ```luau - 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. - - ```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() - -Maps each value in a table source to an object. - -- **Type** - - ```luau - 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. - - ```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 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 6f719e1..d084976 100644 --- a/docs/api/reactivity-utility.md +++ b/docs/api/reactivity-utility.md @@ -1,16 +1,15 @@ -# 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** ```luau - function cleanup(callback: () -> ()) - function cleanup(obj: Destroyable) - function cleanup(obj: Disconnectable) + function cleanup(v: Function | Disconnectable | Destroyable) + type Function = () -> () type Destroyable = { destroy: () -> () } type Disconnectable = { disconnect: () -> () } ``` @@ -18,20 +17,27 @@ Runs a callback anytime a scope is reran or destroyed. - **Example** ```luau - local data = source(1) + 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** @@ -55,16 +61,15 @@ 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** @@ -74,8 +79,8 @@ read can still be tracked inside a reactive scope. ## 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** @@ -86,11 +91,29 @@ trigger effects until after the function finishes running. - **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. @@ -101,15 +124,17 @@ Creates a new 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** @@ -131,4 +156,3 @@ Creates a new context. end) ``` --------------------------------------------------------------------------------- diff --git a/docs/api/strict-mode.md b/docs/api/strict-mode.md index b3cae5b..d3ce5cb 100644 --- a/docs/api/strict-mode.md +++ b/docs/api/strict-mode.md @@ -14,25 +14,23 @@ 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. Better error reporting and stack traces + creation traces of property bindings. -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/tut/advanced/nested-scoping.md b/docs/tut/advanced/nested-scoping.md deleted file mode 100644 index 0542ccc..0000000 --- a/docs/tut/advanced/nested-scoping.md +++ /dev/null @@ -1,192 +0,0 @@ -# Nested Scopes - -Nesting scopes gives you finer control over the reactive graph, but needs more work to do. The built-in control flow functions try to cover the -most common cases, but they do not cover all of them. - -This tutorial will demonstrate how to implement a `show()` control flow function -using just sources and effects. - -```luau -local mount = vide.mount -local source = vide.source -local show = vide.show - -local function Counter() - local count = source(0) - - return create "TextButton" { - Text = count, - Activated = function() count(count() + 1) end - } -end - -root(function() - local toggled = source(true) - - show(toggled, Button) -end) -``` - -```mermaid -%%{init: { - "theme": "base", - "themeVariables": { - "primaryColor": "#1B1B1F", - "primaryTextColor": "#fff", - "primaryBorderColor": "#1B1B1F", - "lineColor": "#79B8FF", - "tertiaryColor": "#161618", - "tertiaryBorderColor": "#1C1C1F" - } -}}%% - -graph - -subgraph mount - direction LR - toggle --> show - - subgraph show[show effect] - text[Text effect] - end -end -``` - -Above is the reactive graph for `show()`. It creates a new effect depending on -`toggle` where anytime `toggle` is truthy, it will create a new `Counter`. The -`show` effect calls `Counter`, which creates a new reactive scope to update its -text whenever `count` changes. As per the rules of reactive scopes, a reactive -scope rerunning will destroy any scopes created within it. So the text -effect's reactive scope is destroyed whenever the show effect is rerun. - -The same can be achieved without the use of `show()`: - -```luau -local mount = vide.mount -local source = vide.source -local effect = vide.effect -local cleanup = vide.cleanup - -local function Counter() - local count = source(0) - - return create "TextButton" { - Text = count, - Activated = function() count(count() + 1) end - } -end - -mount(function() - local toggled = source(true) - - effect(function() - if toggled() then - local destroy = root(function() - Counter() - end) - cleanup(destroy) - end - end) -end) -``` - -```mermaid -%%{init: { - "theme": "base", - "themeVariables": { - "primaryColor": "#1B1B1F", - "primaryTextColor": "#fff", - "primaryBorderColor": "#1B1B1F", - "lineColor": "#79B8FF", - "tertiaryColor": "#161618", - "tertiaryBorderColor": "#1C1C1F" - } -}}%% - -graph - -subgraph mount - direction LR - toggle --> effect - - subgraph effect - subgraph mount2[inner mount] - text[Text effect] - end - end -end -``` - -This is another way to achieve the same. Here we use `root()` within the effect -to manually create and destroy a new stable scope whenever the effect reruns. - -The reason for creating a stable scope is to prevent the effect from tracking -any sources that may be read inside the `Counter()` call. Otherwise, the effect -may be rerun needlessly and recreate the counter. - -Alternatively, instead of using `root()`: - -```luau -local mount = vide.mount -local source = vide.source -local effect = vide.effect -local untrack = vide.untrack - -local function Counter() - local count = source(0) - - return create "TextButton" { - Text = count, - Activated = function() count(count() + 1) end - } -end - -mount(function() - local toggled = source(true) - - effect(function() - if toggled() then - untrack(Button) - end - end) -end) -``` - -```mermaid -%%{init: { - "theme": "base", - "themeVariables": { - "primaryColor": "#1B1B1F", - "primaryTextColor": "#fff", - "primaryBorderColor": "#1B1B1F", - "lineColor": "#79B8FF", - "tertiaryColor": "#161618", - "tertiaryBorderColor": "#1C1C1F" - } -}}%% - -graph - -subgraph mount - direction LR - toggle --> effect - - subgraph effect - text[Text effect] - end -end -``` - -Without the use of `untrack()`, an error would occur, since Vide does not allow -the creation of reactive scopes inside reactive scopes. `untrack()` creates a -stable scope inside the reactive scope, and we can create another reactive scope -inside that stable scope. The -reason for this, is because if the `Counter` component reads from a source -internally, that can cause the reactive scope calling `Counter()` to track that -source, causing unintentional reruns. As a guard against this, you are forced to -use `untrack()` to create nested reactive scopes. - -The final result is the same as using the `show()` component. An effect is -created which creates the counter, which creates its own reactive scope. The -effect rerunning causes the counter's internal reactive scope to be destroyed, -making sure everything is cleaned up. diff --git a/docs/tut/control-flow/1-intro.md b/docs/tut/control-flow/1-intro.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/tut/control-flow/2-show.md b/docs/tut/control-flow/2-show.md deleted file mode 100644 index 8628e45..0000000 --- a/docs/tut/control-flow/2-show.md +++ /dev/null @@ -1 +0,0 @@ -# show() diff --git a/docs/tut/control-flow/3-switch.md b/docs/tut/control-flow/3-switch.md deleted file mode 100644 index 647835a..0000000 --- a/docs/tut/control-flow/3-switch.md +++ /dev/null @@ -1 +0,0 @@ -# switch() diff --git a/docs/tut/control-flow/4-indexes.md b/docs/tut/control-flow/4-indexes.md deleted file mode 100644 index aad2c90..0000000 --- a/docs/tut/control-flow/4-indexes.md +++ /dev/null @@ -1 +0,0 @@ -# indexes() diff --git a/docs/tut/control-flow/5-values.md b/docs/tut/control-flow/5-values.md deleted file mode 100644 index 8909904..0000000 --- a/docs/tut/control-flow/5-values.md +++ /dev/null @@ -1 +0,0 @@ -# values() diff --git a/docs/tut/crash-course/1-introduction.md b/docs/tut/crash-course/1-introduction.md index 983f51b..564aa2d 100644 --- a/docs/tut/crash-course/1-introduction.md +++ b/docs/tut/crash-course/1-introduction.md @@ -6,10 +6,16 @@ Vide is heavily inspired by [Solid](https://www.solidjs.com/). ## Why Vide? -Vide provides a reactive and declarative API to simplify managing UI. +Vide's reactive and declarative API aims to let you program UI as simply as +possible, with a strong focus on how data flows through your application. -Some of the main focuses behind Vide's design choices: +Some of Vide's main design choices: -- Minimal syntax -- Complete typechecking -- Independence from instances +- Syntax minimal. +- Data oriented. +- Typechecking compatible. +- Instance independent. + +Vide's reactivity operates with the concept +of scopes which carries a learning curve, though is what makes Vide's minimal +syntax possible. The crash course will introduce these concepts gradually. diff --git a/docs/tut/crash-course/10-cleanup.md b/docs/tut/crash-course/10-cleanup.md index e498c23..e25d451 100644 --- a/docs/tut/crash-course/10-cleanup.md +++ b/docs/tut/crash-course/10-cleanup.md @@ -9,10 +9,10 @@ destroyed, or when a stable scope is destroyed. local root = vide.root local source = vide.source local effect = vide.effect +local cleanup = vide.cleanup local count = source(0) - local destroy = root(function() effect(function() local x = count() diff --git a/docs/tut/crash-course/11-control-flow.md b/docs/tut/crash-course/11-control-flow.md deleted file mode 100644 index 298ce93..0000000 --- a/docs/tut/crash-course/11-control-flow.md +++ /dev/null @@ -1,98 +0,0 @@ -# Control Flow - -Eventually you may need a way to dynamically create and destroy UI elements -resulting from source updates. Vide provides functions to help you do this, -known as *control flow* functions. - -These functions return new sources, which hold the instances to be displayed. -The new sources can be used in `create()` to update the children of a container -instance. - -## indexes() - -`indexes()` *maps* each table index to a new UI element that can -update to display the current value at that index. Each table index is given a -single corresponding UI element. - -```luau -local list = source { - "finish the crash course", - "star Vide's GitHub" -} - -local function TodoList(props: { list: () -> Array }) - return create "Frame" { - create "UIListLayout" {}, - - indexes(list, function(todo, i) - return create "TextLabel" { - Text = function() - return i .. ": " .. todo() - end, - - LayoutOrder = i - } - end) - } -end - -TodoList { list = list } -``` - -For each index in the given source table, the given function to `indexes()` will -be run in a new stable scope with: - -1. a source containing the value at the index -2. the index itself - -When the value at an index is changed, the function is not reran. Instead, the -given source for that index is updated. - -Any time the input source table is updated, the given function will be ran for -any newly added indexes, while any removed indexes (indexes now with a `nil` -value), will have its corresponding stable scope destroyed. - - - -The reactive graph for the above example: - -```mermaid -%%{init: { - "theme": "base", - "themeVariables": { - "primaryColor": "#1B1B1F", - "primaryTextColor": "#fff", - "primaryBorderColor": "#1B1B1F", - "lineColor": "#79B8FF", - "tertiaryColor": "#161618", - "tertiaryBorderColor": "#1C1C1F" - } -}}%% - -graph - -subgraph root ["root scope"] - direction LR - todoList --> indexes -.- subroot1 & subroot2 - - subgraph subroot1 ["indexes scope 1"] - direction LR - value1[todo] --> prop1["prop binding"] - end - - subgraph subroot2 ["indexes scope 2"] - direction LR - value2[todo] --> prop2[prop binding] - end -end -``` - -When you edit a table in a source, you must set that table again to actually -update the source. - -```luau -local src = source { 1, 2 } -local data = src() -table.insert(data, 3) -- no effects will run -src(data) -- effects will run -``` diff --git a/docs/tut/crash-course/11-dynamic-scope.md b/docs/tut/crash-course/11-dynamic-scope.md new file mode 100644 index 0000000..aa2695b --- /dev/null +++ b/docs/tut/crash-course/11-dynamic-scope.md @@ -0,0 +1,158 @@ +# Dynamic Scoping + +Eventually you may need a way to dynamically create and destroy UI elements +resulting from source updates. Vide provides functions to help you do this, +known as *dynamic scope* functions. + +These functions create and destroy components for you in response to source +updates. They return a source containing the created component. This source can +be parented as a child which will update the shown children whenever the source +updates. + +The simplest example is using `show()`. + +```luau +local source = vide.source +local create = vide.create +local show = vide.show +local root = vide.root + +function Button(props: { Text: string, Activated: () -> () }) + return create "TextButton" { + Text = props.Text, + Activated = props.Activated + } +end + +function Menu() + return create "TextLabel" { + Text = "This is a menu" + } +end + +function App() + local toggled = source(false) + + return create "ScreenGui" { + Button { + Text = "Toggle Menu", + Activated = function() + toggled(not toggled()) + end + }, + + show(toggled, Menu) -- [!code highlight] + } +end + +root(function() + App().Parent = game.StarterGui +end) +``` + +This is a complete example of rendering UI which has a single button that +toggles the opening of a menu. + +-------------------------------------------------------------------------------- + +Another common function is `indexes()`. This function creates a component for +each index in a table. + +Each component created is done so in a new and independent stable scope. The +indexes of the table are checked each source update to prevent redunant +destruction and recreation of UI elements. + +```luau +local source = vide.source +local create = vide.create +local indexes = vide.indexes +local root = vide.root + +local function Todo(props: { + Text: () -> string, + Position: number, + Activated: () -> () +}) + return create "TextButton" { + Text = function() return props.Position .. ": " .. props.Text() end, + LayoutOrder = props.Position, + Activated = Activated + } +end + +local function TodoList(props: { List: () -> Array }) + return create "Frame" { + create "UIListLayout" {}, + + indexes(props.List, function(text, i) -- [!code highlight] + return Todo { + Text = text, + Position = i, + Activated = function() -- remove the todo when clicked + local list = props.List() + table.remove(list, i) + props.List(list) + end + } + end) + } +end + +function App() + local list = source { + "finish the crash course", + "star Vide's GitHub" + } + + return create "ScreenGui" { + TodoList { List = list }, + } +end + +root(function() + App().Parent = game.StarterGui +end) +``` + +The reactive graph for the above example: + +```mermaid +%%{init: { + "theme": "base", + "themeVariables": { + "primaryColor": "#111720", + "primaryTextColor": "#fff", + "primaryBorderColor": "#111720", + "lineColor": "#79B8FF", + "tertiaryColor": "#0d131b", + "tertiaryBorderColor": "#0d131b" + } +}}%% + +graph + +subgraph root ["root"] + direction LR + todoList --> indexes -.- subroot1 & subroot2 + + subgraph subroot1 ["indexes scope 1"] + direction LR + value1[todo] --> prop1["prop binding"] + end + + subgraph subroot2 ["indexes scope 2"] + direction LR + value2[todo] --> prop2[prop binding] + end +end +``` + +When you edit a table in a source, you must set that table again to actually +update the source. + +```luau +local src = source { 1, 2 } +local data = src() +table.insert(data, 3) -- no effects will run +src(data) -- effects will run +``` diff --git a/docs/tut/crash-course/12-actions.md b/docs/tut/crash-course/12-actions.md index 64d3fce..ba02618 100644 --- a/docs/tut/crash-course/12-actions.md +++ b/docs/tut/crash-course/12-actions.md @@ -1,13 +1,11 @@ # Actions -Actions in Vide are special callbacks that you can pass along with properties, +Actions are special callbacks that you can pass along with properties, to run some code on an instance receiving them. ```luau local action = vide.action -``` -```luau create "TextLabel" { Text = "test", @@ -24,6 +22,7 @@ action used to listen for property changes: ```luau local action = vide.action +local source = vide.source local effect = vide.effect local cleanup = vide.cleanup @@ -49,7 +48,7 @@ effect(function() print(output()) end) -instance.Text = "foo" -- "foo" will be printed from the effect +instance.Text = "foo" -- "foo" will be printed by the effect ``` The source `output` will be updated with the new property value any time it is diff --git a/docs/tut/crash-course/13-strict-mode.md b/docs/tut/crash-course/13-strict-mode.md index f2b30fe..0de8887 100644 --- a/docs/tut/crash-course/13-strict-mode.md +++ b/docs/tut/crash-course/13-strict-mode.md @@ -23,6 +23,7 @@ local count = source(0) local ran = 0 effect(function() + count() ran += 1 end) diff --git a/docs/tut/crash-course/14-concepts.md b/docs/tut/crash-course/14-concepts.md index 77e9e7f..8ed6a1a 100644 --- a/docs/tut/crash-course/14-concepts.md +++ b/docs/tut/crash-course/14-concepts.md @@ -30,7 +30,7 @@ Created by: - `root()` - `untrack()` -- `switch()` +- `show()` - `indexes()` Stable scopes do not track sources and never rerun. @@ -47,23 +47,14 @@ Created by: Reactive scopes do track sources and will rerun when those sources update. Reactive scopes cannot be created within a reactive scope, but stable scopes -can. +can be created within a reactive scope. -## Scope Owners +## Scope Cleanup -A scope created within another scope is *owned* by the other scope, with the -exception of the scope created by `root()`. +When a scope is rerun or destroyed, all scopes created within it are +automatically destroyed. -When a scope is rerun or destroyed, all scopes owned by it are automatically -destroyed. - -`root()` creates a stable scope with no owner, instead it is destroyed manually. - -## Cleanup - -Arbitrary code to run whenever a stable or reactive scope is rerun or destroyed. - -Queue a function to run using `cleanup()`. +Any functions queued by `cleanup()` are also ran. ## Reactive Graph @@ -93,12 +84,12 @@ end) %%{init: { "theme": "base", "themeVariables": { - "primaryColor": "#1B1B1F", + "primaryColor": "#111720", "primaryTextColor": "#fff", - "primaryBorderColor": "#1B1B1F", + "primaryBorderColor": "#111720", "lineColor": "#79B8FF", - "tertiaryColor": "#161618", - "tertiaryBorderColor": "#1C1C1F" + "tertiaryColor": "#0d131b", + "tertiaryBorderColor": "#202530" } }}%% @@ -118,6 +109,5 @@ Notes: - An update to `count` will cause `text` to rerun, which then causes `effect` to rerun. - When the root scope is destroyed, `text` and - `effect` will be destroyed alongside it, since they are - owned by it. `count` will be untouched and future updates - to `count` will have no effect. + `effect` will be destroyed alongside it, since they were created within it. + `count` will be untouched and future updates to `count` will have no effect. diff --git a/docs/tut/crash-course/5-effect.md b/docs/tut/crash-course/5-effect.md index 29e6358..e435034 100644 --- a/docs/tut/crash-course/5-effect.md +++ b/docs/tut/crash-course/5-effect.md @@ -1,6 +1,6 @@ # Effects -Effects are functions that are ran in response to source updates. They are +Effects are functions that are ran in response to source updates. A source and effect is analogous to a signal and connection. Effects are created using `effect()`. @@ -23,7 +23,10 @@ count(1) Any source read inside an effect is tracked and will rerun the effect when that source is updated. -Derived sources are also tracked, it doesn't matter how deeply nested +The effect runs its callback once immediately to initially figure out what +sources are being read. + +Derived sources are also tracked, it does not matter how deeply nested inside a function a source is. ```luau @@ -47,3 +50,22 @@ count(2) If a source is updated with the same value it already had, it will not rerun effects depending on it. + +You can also read from a source within an effect without the effect tracking it. + +```lua +local source = vide.source +local effect = vide.effect +local untrack = vide.untrack + +local a = source(0) +local b = source(0) + +effect(function() + print(`a: {a()} b: {untrack(b)}`) +end) + +a(1) -- prints "a: 1 b: 0" +b(1) -- prints nothing +a(2) -- prints "a: 2 b: 1" +``` diff --git a/docs/tut/crash-course/6-scope.md b/docs/tut/crash-course/6-scope.md index 6c12088..a222e50 100644 --- a/docs/tut/crash-course/6-scope.md +++ b/docs/tut/crash-course/6-scope.md @@ -7,29 +7,31 @@ But the disconnecting of many signals and connections is tedious and verbose. Vide instead operates on the concept of scopes which provides a much cleaner API, given that you follow a few rules. -Scopes come in two flavors; stable and reactive. +Thre are two types of scopes: stable and reactive. -- All scopes must be created within another scope with the exception of `root()` -- Stable scopes never rerun -- Reactive scopes can rerun -- A reactive scope cannot be created within another reactive scope +- A scope must be created within another scope. +- Stable scopes never rerun. +- Reactive scopes can rerun. +- A reactive scope cannot be created within another reactive scope, only within + a stable scope. + +An exception to the first rule is `root()`, which creates the initial scope that +you destroy manually with a destructor function it returns. -`effect()` creates a reactive scope. `root()` creates a stable scope. +`effect()` creates a reactive scope. Whenever a scope is destroyed, any scope created within that scope is also -destroyed, and so on. This is why all scopes must be created within another -scope, except `root()` which is used to create the initial scope that you can -manually destroy. +destroyed, and so on. ```luau local root = vide.root local source = vide.source local effect = vide.effect -local function setup() - local count = source(0) +local count = source(0) +local function setup() effect(function() print(count()) end) @@ -37,32 +39,16 @@ local function setup() return count end -setup() -- will error since effect() tries to create a reactive scope outside of a stable scope +setup() -- error, effect() tried to create a reactive scope with no stable scope -local count = root(setup) -- ok since effect() was called within a stable scope -count(1) -- prints "1" -``` - -The scope created by `root()` can be destroyed. - -```luau -local function setup() - local count = source(0) - - effect(function() - print(count()) - end) - - return count -end - -local destroy, count = root(setup) +local destroy = root(setup) -- ok since effect() was called in a stable scope count(1) -- prints "1" +count(2) -- prints "2" destroy() -count(2) -- effect is destroyed; no longer prints +count(3) -- reactive scope created by effect() is destroyed, it does not rerun ``` Vide's reactivity can be represented graphically, as a *reactive graph*. @@ -73,12 +59,12 @@ The reactive graph for the above example looks like so: %%{init: { "theme": "base", "themeVariables": { - "primaryColor": "#1B1B1F", + "primaryColor": "#111720", "primaryTextColor": "#fff", - "primaryBorderColor": "#1B1B1F", + "primaryBorderColor": "#111720", "lineColor": "#79B8FF", - "tertiaryColor": "#161618", - "tertiaryBorderColor": "#161618" + "tertiaryColor": "#0d131b", + "tertiaryBorderColor": "#0d131b" } }}%% @@ -90,7 +76,7 @@ subgraph root end ``` -When the stable `root()` is destroyed, the reactive `effect()` +When the stable `root()` scope is destroyed, the reactive `effect()` scope will also be destroyed since it was created within it. This is important because you may have an effect that updates the property of a diff --git a/docs/tut/crash-course/7-stateful-component.md b/docs/tut/crash-course/7-reactive-component.md similarity index 91% rename from docs/tut/crash-course/7-stateful-component.md rename to docs/tut/crash-course/7-reactive-component.md index 5126c73..ff67ec8 100644 --- a/docs/tut/crash-course/7-stateful-component.md +++ b/docs/tut/crash-course/7-reactive-component.md @@ -1,10 +1,8 @@ -# Stateful Components +# Reactive Components -Stateful components in Vide are created using sources and effects - sources to +Reactive components in Vide are created using sources and effects - sources to store the data, and effects to display the data. -## Internal State - ```luau local create = vide.create local source = vide.source @@ -33,8 +31,6 @@ its internal count, and automatically update its text to reflect that count. Each instance of `Counter()` will maintain its own independent count, since the count source is created inside the component. -## External State - External sources can also be passed into components for them to use. ```luau diff --git a/docs/tut/crash-course/8-implicit-effect.md b/docs/tut/crash-course/8-implicit-effect.md index cdb75fb..3363bf6 100644 --- a/docs/tut/crash-course/8-implicit-effect.md +++ b/docs/tut/crash-course/8-implicit-effect.md @@ -3,7 +3,9 @@ Explicitly creating effects to update properties is tedious. You can *implicitly* create an effect to update properties instead. -```luau +::: code-group + +```luau [Implicit Effect] local create = vide.create local source = vide.source @@ -22,6 +24,30 @@ local function Counter() end ``` +```luau [Explicit Effect] +local create = vide.create +local source = vide.source +local effect = vide.effect + +local function Counter() + local count = source(0) + + local instance = create "TextButton" { + Activated = function() + count(count() + 1) + end + } + + effect(function() + instance.Text = "count: " .. count() + end) + + return instance +end +``` + +::: + This example is equivalent to the example seen on the previous page. Instead of explicitly creating an effect, assigning a (non-event) property a @@ -46,12 +72,12 @@ local function List(props: { children: () -> { Instance } }) } end -local list = List { children = items } -- creates a list with a single text label "A" +local list = List { children = items } -- creates a list with text label "A" items { create "TextLabel" { Text = "B" }, create "TextLabel" { Text = "C" } } --- this will automatically unparent the text label "A", and parent the labels "B" and "C" +-- this will automatically unparent text label "A", and parent labels "B" and "C" ``` diff --git a/docs/tut/crash-course/9-derived-source.md b/docs/tut/crash-course/9-derived-source.md index 19a6a5d..694d90b 100644 --- a/docs/tut/crash-course/9-derived-source.md +++ b/docs/tut/crash-course/9-derived-source.md @@ -57,7 +57,8 @@ effect(function() text() end) count(1) -- prints "ran" x1 ``` -`derive()` must also be called within a stable scope, just like `effect()`. +Because `derive()` creates a reactive scope, it must be called within a stable +scope, just like `effect()`. If the recalculated value is the same as the old value, the derived source will not rerun the effects using it. @@ -68,12 +69,12 @@ The reactive graph for the above example: %%{init: { "theme": "base", "themeVariables": { - "primaryColor": "#1B1B1F", + "primaryColor": "#111720", "primaryTextColor": "#fff", - "primaryBorderColor": "#1B1B1F", + "primaryBorderColor": "#111720", "lineColor": "#79B8FF", - "tertiaryColor": "#161618", - "tertiaryBorderColor": "#161618" + "tertiaryColor": "#0d131b", + "tertiaryBorderColor": "#0d131b" } }}%% @@ -86,7 +87,7 @@ end ``` Deriving a source in this manner is similar to creating an effect to update -another source. You should never manually do this using an effect however. +another source. You should avoid doing this using an effect however. Improper usage could accidently create infinite loops in the reactive graph. Always favour deriving when you need one source to update based on another source. diff --git a/docs/tut/dynamic-scoping/custom.md b/docs/tut/dynamic-scoping/custom.md new file mode 100644 index 0000000..fc4258b --- /dev/null +++ b/docs/tut/dynamic-scoping/custom.md @@ -0,0 +1,144 @@ +# Dynamic Scoping + +Dynamic scoping is the act of creating and destroying new scopes in response to +source updates. This is needed for conditionally rendering parts of your UI, +such as opening and closing menus. + +While Vide provides functions for common ways to do this, this section will +show how you can implement them yourself so you are not limited by only what is +provided. + +## Recreating [`show()`](/api/reactivity-dynamic#show-reactive) + +The most basic one, `show()`, can be +implemented yourself like so: + +```luau +local function show(toggle: () -> unknown, component: () -> Instance) + return derive(function() + return if toggle() then untrack(component) else nil + end) +end +``` + +The main thing to note here is the use of `untrack()`. This function runs its +callback in a new stable scope. Without this, if the component were to create +a reactive scope, an error would occur since a reactive scope cannot be created +within a reactive scope. + +```mermaid +%%{init: { + "theme": "base", + "themeVariables": { + "primaryColor": "#111720", + "primaryTextColor": "#fff", + "primaryBorderColor": "#444455", + "lineColor": "#79B8FF", + "tertiaryColor": "#0d131b", + "tertiaryBorderColor": "#444455" + } +}}%% + +graph + +subgraph derive ["derive (reactive)"] + + subgraph untrack ["untrack (stable)"] + subgraph effect ["effect (reactive)"] + + end + end +end +``` + +You can see from the above graph how the effect would not be created directly +inside the derive, there is a stable scope between them. This requirement exists +as a guard against unintentional rerendering of UI. + +## Recreating [`switch()`](/api/reactivity-dynamic#switch-reactive) + +```lua +local function switch(key) + return function(map) + return derive(function() + local component = map[key()] + return if component then untrack(component) else nil + end) + end +end +``` + +## Recreating [`indexes()`](/api/reactivity-dynamic#indexes-reactive) + +This is a more complicated function because it manages multiple scopes at the +same time, unlike the previous functions. Because some scopes may persist +between reruns, we cannot use `untrack()` anymore which automatically destroys +on rerun; we must use `root()` where the lifetime of each scope is managed +manually and independently. + + +```lua +local function indexes( + input: () -> Map, + transform: (value: () -> VI, index: I) -> VO +) + local index_caches = {} :: Map VI, + destroy: () -> () + }?> + + return derive(function() + local new_input = input() + + -- destroy scopes of removed indexes + for i, cache in index_caches do + if new_input[i] == nil then + assert(cache).destroy() + index_caches[i] = nil + end + end + + -- create scopes or update sources of added or changed index values + for i, v in new_input do + local cache = index_caches[i] + + if cache == nil then -- no scope created for this index, create one + local src = source(v) + + local destroy, result = root(function() + return transform(src, i) + end) + + index_caches[i] = { + destroy = destroy, + source = src, + output = result, + previous_input = v + } + elseif cache.previous_input ~= v then -- scope exists, update source + cache.previous_input = v + cache.source(v) + else -- scope exists and value has not changed; do nothing + end + end + + -- return the cached output values as an array + local array = table.create(#index_caches) + + for _, cache in index_caches do + table.insert(array, assert(cache).output) + end + + return array + end) +end +``` + +-------------------------------------------------------------------------------- + +Though the above functions are already provided to you by Vide, this serves as +an example for how you may create your own dynamic scope functions. + +