diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index b889dc8..ae4c9e5 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -53,17 +53,9 @@ export default defineConfig({
]
},
{
- text: "Control Flow WIP",
+ text: "Advanced Reactivity",
items: [
- { text: "switch", link: "/tut/control-flow/switch.md" },
- { text: "indexes", link: "/tut/control-flow/indexes.md" },
- { text: "values", link: "/tut/control-flow/values.md" },
- ]
- },
- {
- text: "Advanced Reactivity WIP",
- items: [
- { text: "reactive-scopes", link: "/tut/reactive-scoping.md"}
+ { text: "Reactive Scopes", link: "/tut/advanced/reactive-scoping.md"}
]
}
],
diff --git a/docs/api/reactivity-flow.md b/docs/api/reactivity-flow.md
index 58e2200..47dafa8 100644
--- a/docs/api/reactivity-flow.md
+++ b/docs/api/reactivity-flow.md
@@ -2,9 +2,33 @@
+## show()
+
+Shows one of two components depending on an input source.
+
+- **Type**
+
+ ```lua
+ function show(source: () -> unknown, component: () -> T): () -> T?
+ function show(source: () -> unknown, component: () -> T, fallback: () -> U): () -> T | U
+ ```
+
+- **Details**
+
+ Returns a source holding an instance of the currently shown component.
+
+ When the input source changes from a falsey to a truthy value, the
+ component will be reran under a new reactive scope. If it changes from a
+ truthy to falsey value, the reactive scope the component was created in will
+ be destroyed, and the returned source will output `nil`, or a fallback
+ component if given.
+
+ The fallback component is also ran under a new reactive scope, and destroyed
+ when the input source switches back to truthy.
+
## switch()
-Changes object based on a source and a mapping table.
+Shows one of a set of components depending on an input source and a mapping table.
- **Type**
@@ -14,12 +38,14 @@ Changes object based on a source and a mapping table.
- **Details**
- The mapped function is ran in a new reactive scope that is destroyed when
- the source changes and maps to a different function.
+ Returns a source holding an instance of the currently shown component.
- ::: warning
- Mapped functions cannot yield.
- :::
+ When the input source changes, the new value will be used to lookup a given
+ mapping table to get a component, which will be ran under a new reactive
+ scope. If the input source changes, the reactive scope the component was
+ created in will be destroyed, and a new component created under a new
+ reactive scope. If no component is found for an input value, the switch will
+ output `nil`.
- **Example**
@@ -51,24 +77,26 @@ Maps each index in a table source to an object.
- **Details**
+ Returns a source holding an array of instances currently shown.
+
+ When the input source changes, each *index* in the new table is compared with
+ the last input table.
+
+ - For any new index, the `transform` function is ran under a new reactive
+ scope to produce a new instance.
+ - For any removed index, the reactive scope for that index is destroyed.
+ - Unchanged indexes are untouched.
+
The transform function is called only ever *once* for each index in the
- source table. The first argument is a source containing the index's value
- and the second argument is just the index.
+ source table.
- Anytime a new index is added, the transform function will be called again
- for that new index.
+ 1. First argument is a *source containing the index's value*.
+ 2. Second argument is the *index itself*.
- Anytime an existing index value changes, the transform function is not rerun,
- instead the source value for that index will update, causing anything
+ 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.
- Returns a state containing an array of all objects returned by the
- transform.
-
- ::: warning
- `transform()` cannot yield.
- :::
-
- **Example**
The intended purpose of this function is to map each index in a table to
@@ -85,14 +113,12 @@ Maps each index in a table source to an object.
local displays = indexes(items, function(item, i)
return ItemDisplay {
Name = function()
- return item().name
+ return i .. ": " .. item().name
end,
Image = function()
return "rbxassetid://" .. item().icon
end,
-
- LayoutOrder = i
}
end)
```
@@ -111,28 +137,31 @@ Maps each value in a table source to an object.
- **Details**
- The transform function is called only ever *once* for each value in the
- source table. The first argument is the index's value and
- the second argument is a source containing the index.
+ Returns a source holding an array of instances currently shown.
- Anytime a new value is added, the transform function will be called again
- for that new value.
+ When the input source changes, each *value* in the new table is compared with
+ the last input table. Similar to `indexes()` but for values instead of indexes.
+
+ - For any new value, the `transform` function is ran under a new reactive
+ scope to produce a new instance.
+ - For any removed value, the reactive scope for that value is destroyed.
+ - Unchanged values are untouched.
+
+ The transform function is only ever called *once* for each value in the
+ source table.
+
+ 1. First argument is the *value itself*.
+ 2. Second argument is a *source containing the value's index*.
Anytime an existing value's index changes, the transform function is not
rerun, instead the source index for that value will update, causing anything
depending on it to update too.
- Returns a state containing an array of all objects returned by the
- transform.
-
::: warning
- `transform()` cannot yield.
- :::
-
- ::: warning
- Having primitive values in the source table can cause unexpected behavior,
- as duplicate primitives can result in multiple index sources being bound
- to the same UI element.
+ 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**
@@ -150,11 +179,11 @@ Maps each value in a table source to an object.
local displays = values(items, function(item, i)
return ItemDisplay {
- Name = item.Name
+ Name = function()
+ return i() .. ": " .. item.Name
+ end
Image = "rbxassetid://" .. item.icon,
-
- LayoutOrder = i
}
end)
```
@@ -181,6 +210,9 @@ Maps each value in a table source to an object.
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.
+ result in less property updates and less re-renders. One case to note is
+ that `values()` works nicely when animating re-ordering of instances, since
+ the value is not destroyed when indexes are changed, and the source index
+ can easily be put through a spring.
--------------------------------------------------------------------------------
diff --git a/docs/api/reactivity-utility.md b/docs/api/reactivity-utility.md
index 22adba5..9219b29 100644
--- a/docs/api/reactivity-utility.md
+++ b/docs/api/reactivity-utility.md
@@ -24,20 +24,6 @@ Runs a callback anytime a reactive scope is re-ran.
end)
```
- ```lua
- local data = source(1)
-
- derive(function()
- local label = create "TextLabel" { Text = data() }
-
- cleanup(function()
- label:Destroy()
- end)
-
- return label
- end)
- ```
-
## untrack()
Runs a given function where any sources read will not track its reactive scope.
@@ -70,4 +56,14 @@ Runs a given function where any sources read will not track its reactive scope.
print(sum()) -- 2
```
+## read()
+
+Utility used to read a value that is either a primitive or a source.
+
+- **Type**
+
+ ```lua
+ function read(value: T | () -> T): T
+ ```
+
--------------------------------------------------------------------------------
diff --git a/docs/api/strict-mode.md b/docs/api/strict-mode.md
index 91be656..35de4d8 100644
--- a/docs/api/strict-mode.md
+++ b/docs/api/strict-mode.md
@@ -6,6 +6,9 @@ Strict mode is library-wide and can get set by doing:
vide.strict = true
```
+It is automatically enabled when Vide is first required and not running in O2
+optimization level.
+
Strict mode is designed to help the development process by adding safety checks
and identifying improper usage.
@@ -15,8 +18,9 @@ Currently, strict mode will:
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 duplicate nested properties at same depth.
-6. Better error reporting and stack traces + creation traces of property bindings.
+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.
By rerunning sources and effects, any side-effects are made more apparent.
This also helps ensure that cleanups are being handled correctly.
@@ -29,4 +33,5 @@ recording and better emitting stack traces where errors occur, particularly
when binding properties to sources.
It is recommend to develop UI with strict mode and to disable it when pushing to
-production.
+production. In Roblox, production code compiles at O2 by default, so you don't
+need to worry about disabling strict mode unless you have manually enabled it.
diff --git a/docs/tut/reactive-scoping.md b/docs/tut/advanced/reactive-scoping.md
similarity index 100%
rename from docs/tut/reactive-scoping.md
rename to docs/tut/advanced/reactive-scoping.md
diff --git a/docs/tut/control-flow/1-intro.md b/docs/tut/control-flow/1-intro.md
new file mode 100644
index 0000000..e69de29
diff --git a/docs/tut/control-flow/2-show.md b/docs/tut/control-flow/2-show.md
new file mode 100644
index 0000000..8628e45
--- /dev/null
+++ b/docs/tut/control-flow/2-show.md
@@ -0,0 +1 @@
+# show()
diff --git a/docs/tut/control-flow/3-switch.md b/docs/tut/control-flow/3-switch.md
new file mode 100644
index 0000000..647835a
--- /dev/null
+++ b/docs/tut/control-flow/3-switch.md
@@ -0,0 +1 @@
+# switch()
diff --git a/docs/tut/control-flow/4-indexes.md b/docs/tut/control-flow/4-indexes.md
new file mode 100644
index 0000000..aad2c90
--- /dev/null
+++ b/docs/tut/control-flow/4-indexes.md
@@ -0,0 +1 @@
+# indexes()
diff --git a/docs/tut/control-flow/5-values.md b/docs/tut/control-flow/5-values.md
new file mode 100644
index 0000000..8909904
--- /dev/null
+++ b/docs/tut/control-flow/5-values.md
@@ -0,0 +1 @@
+# values()
diff --git a/docs/tut/control-flow/indexes.md b/docs/tut/control-flow/indexes.md
deleted file mode 100644
index 70864e7..0000000
--- a/docs/tut/control-flow/indexes.md
+++ /dev/null
@@ -1,66 +0,0 @@
-# Control Flow
-
-Vide has specific functions for dealing with sources that store a table value.
-
-Often, you will have a table of values that will be displayed in a similar
-manner. Rather than manually looping over each value to generate a corresponding
-UI element, Vide provides functions `indexes()` and `values()` to do this for
-you.
-
-`indexes()` maps each *index* in a table to a UI element.
-
-```lua
-local names = source { "a", "b", "c" }
-
-local elements = indexes(names, function(name, i)
- return create "TextLabel" {
- Text = function()
- return "Name: " .. name()
- end,
-
- LayoutOrder = i
- }
-end)
-```
-
-What happens here is the given callback is only ever ran *once* for each index
-in the table. The callback receives two arguments, a *source* containing the
-index's value and then the index itself.
-
-Anytime the value at a corresponding index changes, the source for that index
-value is updated, causing the UI element depending on it to update too.
-
-`values()` behaves similarly, except it maps each *value* in a table to a UI
-element.
-
-```lua
-type Item = {
- Name: string,
- Icon: number
-}
-
-local items = source({} :: Array- )
-
-local elements = values(items, function(item, i)
- return create "ImageLabel" {
- Image = "rbxassetid://" .. item.Icon,
- LayoutOrder = i
- }
-end)
-```
-
-The callback is again only ever ran *once* for each value in the table. The
-callback receives two arguments, a value in the table and then a *source*
-containing the value's corresponding index.
-
-Any time a value in a table changes index, the source for that value is updated,
-causing the UI element position to change.
-
-In certain cases `values()` can cause less recalculation and rerenders than
-`indexes()` like when items are re-arranged and shifted within a table.
-
-It is important that each value in a table is unique when using `values()`,
-and for this reason always using `indexes()` if a table contains primitive
-values.
-
-Both `indexes()` and `values()` return an array of all mapped UI elements.
diff --git a/docs/tut/control-flow/switch.md b/docs/tut/control-flow/switch.md
deleted file mode 100644
index 70864e7..0000000
--- a/docs/tut/control-flow/switch.md
+++ /dev/null
@@ -1,66 +0,0 @@
-# Control Flow
-
-Vide has specific functions for dealing with sources that store a table value.
-
-Often, you will have a table of values that will be displayed in a similar
-manner. Rather than manually looping over each value to generate a corresponding
-UI element, Vide provides functions `indexes()` and `values()` to do this for
-you.
-
-`indexes()` maps each *index* in a table to a UI element.
-
-```lua
-local names = source { "a", "b", "c" }
-
-local elements = indexes(names, function(name, i)
- return create "TextLabel" {
- Text = function()
- return "Name: " .. name()
- end,
-
- LayoutOrder = i
- }
-end)
-```
-
-What happens here is the given callback is only ever ran *once* for each index
-in the table. The callback receives two arguments, a *source* containing the
-index's value and then the index itself.
-
-Anytime the value at a corresponding index changes, the source for that index
-value is updated, causing the UI element depending on it to update too.
-
-`values()` behaves similarly, except it maps each *value* in a table to a UI
-element.
-
-```lua
-type Item = {
- Name: string,
- Icon: number
-}
-
-local items = source({} :: Array
- )
-
-local elements = values(items, function(item, i)
- return create "ImageLabel" {
- Image = "rbxassetid://" .. item.Icon,
- LayoutOrder = i
- }
-end)
-```
-
-The callback is again only ever ran *once* for each value in the table. The
-callback receives two arguments, a value in the table and then a *source*
-containing the value's corresponding index.
-
-Any time a value in a table changes index, the source for that value is updated,
-causing the UI element position to change.
-
-In certain cases `values()` can cause less recalculation and rerenders than
-`indexes()` like when items are re-arranged and shifted within a table.
-
-It is important that each value in a table is unique when using `values()`,
-and for this reason always using `indexes()` if a table contains primitive
-values.
-
-Both `indexes()` and `values()` return an array of all mapped UI elements.
diff --git a/docs/tut/control-flow/values.md b/docs/tut/control-flow/values.md
deleted file mode 100644
index 70864e7..0000000
--- a/docs/tut/control-flow/values.md
+++ /dev/null
@@ -1,66 +0,0 @@
-# Control Flow
-
-Vide has specific functions for dealing with sources that store a table value.
-
-Often, you will have a table of values that will be displayed in a similar
-manner. Rather than manually looping over each value to generate a corresponding
-UI element, Vide provides functions `indexes()` and `values()` to do this for
-you.
-
-`indexes()` maps each *index* in a table to a UI element.
-
-```lua
-local names = source { "a", "b", "c" }
-
-local elements = indexes(names, function(name, i)
- return create "TextLabel" {
- Text = function()
- return "Name: " .. name()
- end,
-
- LayoutOrder = i
- }
-end)
-```
-
-What happens here is the given callback is only ever ran *once* for each index
-in the table. The callback receives two arguments, a *source* containing the
-index's value and then the index itself.
-
-Anytime the value at a corresponding index changes, the source for that index
-value is updated, causing the UI element depending on it to update too.
-
-`values()` behaves similarly, except it maps each *value* in a table to a UI
-element.
-
-```lua
-type Item = {
- Name: string,
- Icon: number
-}
-
-local items = source({} :: Array
- )
-
-local elements = values(items, function(item, i)
- return create "ImageLabel" {
- Image = "rbxassetid://" .. item.Icon,
- LayoutOrder = i
- }
-end)
-```
-
-The callback is again only ever ran *once* for each value in the table. The
-callback receives two arguments, a value in the table and then a *source*
-containing the value's corresponding index.
-
-Any time a value in a table changes index, the source for that value is updated,
-causing the UI element position to change.
-
-In certain cases `values()` can cause less recalculation and rerenders than
-`indexes()` like when items are re-arranged and shifted within a table.
-
-It is important that each value in a table is unique when using `values()`,
-and for this reason always using `indexes()` if a table contains primitive
-values.
-
-Both `indexes()` and `values()` return an array of all mapped UI elements.
diff --git a/docs/tut/crash-course/12-strict-mode.md b/docs/tut/crash-course/12-strict-mode.md
index c66af04..3cb09e6 100644
--- a/docs/tut/crash-course/12-strict-mode.md
+++ b/docs/tut/crash-course/12-strict-mode.md
@@ -5,5 +5,27 @@ be set with `vide.strict = true` once when you first require Vide. Strict mode
will add extra safety checks and emit better error traces, particularly when
errors occur in property bindings.
+Strict mode will run derived sources and effects twice each time they update.
+This is to help identify improper cleanup of side-effects and ensure that pure
+computations are actually pure.
+
+```lua
+local source = vide.source
+local effect = vide.effect
+
+vide.strict = true
+
+local count = source(0)
+
+local ran = 0
+effect(function()
+ ran += 1
+end)
+
+print(ran) -- 2
+count(1)
+print(ran) -- 4
+```
+
A full list of what strict mode will do can be found
[here](../../api/strict-mode).
diff --git a/docs/tut/crash-course/9-control-flow.md b/docs/tut/crash-course/9-control-flow.md
index fdde0ae..8e19fe2 100644
--- a/docs/tut/crash-course/9-control-flow.md
+++ b/docs/tut/crash-course/9-control-flow.md
@@ -1,56 +1,105 @@
# Control Flow
Eventually you will need a way to dynamically create and destroy UI elements
-resulting from state changes. Vide provides functions to help you do this,
+resulting from source updates. Vide provides functions to help you do this,
known as *control flow* functions.
-These functions return a new source, which holds the instances to be displayed.
+These functions return new sources, which hold the instances to be displayed.
These sources can be assigned as children, meaning the displayed children
will update when the input source updates.
-One of these functions is `switch()`, used to conditionally show one of a set of
-components.
+Control flow functions are special, because they run their components in a new
+reactive scope, which can be destroyed independently of the reactive scope that
+called the control flow function itself. This means that parts of your app can
+be independently created then destroyed and cleaned.
+
+## show()
+
+The most basic control flow function is `show()`, which is used to conditionally
+show a component.
+
+```lua
+local source = vide.source
+local show = vide.show
+
+local function JoinMenu()
+ local joined = source(false)
+
+ local function JoinButton()
+ return Button {
+ Activated = function() joined(true) end
+ }
+ end
+
+ return create "Frame" {
+ show(function() return not joined() end, JoinButton)
+ }
+end
+```
+
+This will make a button to join if you have not joined already.
+
+You can also pass a third argument, a fallback to show if the condition is falsey.
+
+```lua
+local function JoinMenu()
+ local joined = source(false)
+
+ local function JoinButton()
+ return Button {
+ Activated = function() joined(true) end
+ }
+ end
+
+ local function LeaveButton()
+ return Button {
+ Activated = function() joined(false) end
+ }
+ end
+
+ return create "Frame" {
+ show(joined, LeaveButton, JoinButton)
+ }
+end
+```
+
+## switch()
+
+Similar to `show()`, `switch()`, also condtionally displays one instance at a
+time. It is more flexible since it can show one of many components, based on a
+table used to map a source value to a component.
```lua
-local vide = require(vide)
local source = vide.source
local switch = vide.switch
-local function ToggleButton(p: {
- Text: string,
- Toggle: (boolean) -> boolean
-})
- return create "TextButton" {
- Size = UDim2.fromOffset(300, 300),
- Text = p.Text,
- Activated = function()
- p.Toggle(not p.Toggle())
- end
- }
-end
+local function JoinMenu()
+ local joined = source(false)
-local loggedIn = source(false)
+ local function JoinButton()
+ return Button {
+ Activated = function() joined(true) end
+ }
+ end
-local function LoginMenu()
- return Frame {
- switch(loggedIn) {
- [true] = function()
- return ToggleButton { Text = "Log out", Toggle = loggedIn }
- end,
+ local function LeaveButton()
+ return Button {
+ Activated = function() joined(false) end
+ }
+ end
- [false] = function()
- return ToggleButton { Text = "Log in", Toggle = loggedIn }
- end
+ return create "Frame" {
+ switch(joined) {
+ [true] = LeaveButton,
+ [false] = JoinButton
}
}
end
-
-mount(function() return create "ScreenGui" { LoginMenu {} } end, game.StarterGui)
```
-Above is an example of using a switch to create a login menu. Each time
-`loggedIn` toggles, the current button will be destroyed, and a new button
-created, which the text to represent the current action, to log in or log out.
+Above is an example of using a switch to create a join menu. Each time
+`joined` toggles, the current button will be destroyed, and a new button
+created, which the text to represent the current action, to join or leave.
The callbacks given to control flow functions are ran in a new reactive-scope,
so any cleanups registered will be ran when the input is changed and a new