mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
Merge reactive scope refactor
This commit is contained in:
parent
0e439f084f
commit
efc4798ddb
48 changed files with 2750 additions and 1949 deletions
|
|
@ -26,6 +26,7 @@ export default defineConfig({
|
|||
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" },
|
||||
|
|
@ -41,16 +42,26 @@ export default defineConfig({
|
|||
{ text: "Element Creation", link: "/tut/crash-course/2-creation" },
|
||||
{ text: "Components", link: "/tut/crash-course/3-components" },
|
||||
{ text: "Source", link: "/tut/crash-course/4-source" },
|
||||
{ text: "Derived Source", link: "/tut/crash-course/5-derived-source" },
|
||||
{ text: "Table Source", link: "/tut/crash-course/6-table-source" },
|
||||
{ text: "Nested Properties", link: "/tut/crash-course/7-nested-properties" },
|
||||
{ text: "Actions", link: "/tut/crash-course/8-actions" },
|
||||
{ text: "Effect", link: "/tut/crash-course/5-effect" },
|
||||
{ text: "Derived Source", link: "/tut/crash-course/6-derived-source" },
|
||||
{ text: "Cleanup", link: "/tut/crash-course/7-cleanup" },
|
||||
{ text: "Control Flow", link: "/tut/crash-course/8-control-flow" },
|
||||
{ text: "Property Nesting", link: "/tut/crash-course/9-property-nesting" },
|
||||
{ text: "Actions", link: "/tut/crash-course/10-actions" },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: "Tutorials",
|
||||
text: "Control Flow WIP",
|
||||
items: [
|
||||
{ text: "Crash Course", link: "/tut/crash-course/index" },
|
||||
{ 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"}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -2,6 +2,38 @@
|
|||
|
||||
<br/>
|
||||
|
||||
## mount()
|
||||
|
||||
Runs a function and applies its result to a target instance.
|
||||
|
||||
- **Type**
|
||||
|
||||
```lua
|
||||
function mount<T>(component: () -> T, target: Instance?): () -> ()
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
The result of the function is applies to the target in the same way
|
||||
properties are using `create()`.
|
||||
|
||||
The function is ran in a new reactive scope, just like
|
||||
[root()](reactivity-core.md#root).
|
||||
|
||||
Returns a function that when called will destroy the reactive scope.
|
||||
|
||||
- **Example**
|
||||
|
||||
```lua
|
||||
local function App()
|
||||
return create "ScreenGui" {
|
||||
create "TextLabel" { Text = "Vide" }
|
||||
}
|
||||
end
|
||||
|
||||
mount(App, game.StarterGui)
|
||||
```
|
||||
|
||||
## create()
|
||||
|
||||
Creates a new UI element, applying any given properties.
|
||||
|
|
@ -28,19 +60,18 @@ Creates a new UI element, applying any given properties.
|
|||
|
||||
- **Property setting rules**
|
||||
|
||||
|
||||
- If a table index is a string:
|
||||
- If its value is a table then it will attempt to perform aggregate
|
||||
initialization.
|
||||
- If its value is a function then it will either bind that property to
|
||||
the function or connect it if the property type is a `RBXScriptSignal`.
|
||||
- If the value is not a function then the property will be set to that
|
||||
value.
|
||||
- If a table index is a number:
|
||||
- If its value is a table then that table will be recursively
|
||||
- processed just like the outer table.
|
||||
- If its value is a function then it will parent and bind any instances
|
||||
returned by that function as children.
|
||||
- If its value is an action then that action will be queued to run after
|
||||
properties are set.
|
||||
- If its value is a table then that table will be recursively
|
||||
processed just like the outer table.
|
||||
- If its value is a function then it will bind the instances children to
|
||||
that function.
|
||||
- If its value is an instance then it will be parented to the instance.
|
||||
|
||||
- **Example**
|
||||
|
|
@ -54,7 +85,7 @@ Creates a new UI element, applying any given properties.
|
|||
}
|
||||
```
|
||||
|
||||
A component using property nesting/grouping.
|
||||
A component using property nesting.
|
||||
|
||||
```lua
|
||||
type Layout = {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,30 @@
|
|||
|
||||
<br/>
|
||||
|
||||
## root()
|
||||
|
||||
Creates and runs a function in a new reactive scope.
|
||||
|
||||
- **Type**
|
||||
|
||||
```lua
|
||||
function root<T...>(fn: (destroy: () -> ()) -> T...): T...
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
Creates a new root reactive scope, where creation and derivations of sources
|
||||
can be tracked and properly disposed of.
|
||||
|
||||
Returns the result of the given function.
|
||||
|
||||
A function to destroy the root is passed into the callback, which will run
|
||||
any cleanups and allow derived sources created to garbage collect.
|
||||
|
||||
::: warning
|
||||
`fn()` cannot yield.
|
||||
:::
|
||||
|
||||
## source()
|
||||
|
||||
Creates a new source with the given value.
|
||||
|
|
@ -14,10 +38,10 @@ Creates a new source with the given value.
|
|||
|
||||
- **Details**
|
||||
|
||||
Calling the returned source with no arguments will return its stored value,
|
||||
calling with arguments will set a new value.
|
||||
Calling the returned source with no argument will return its stored value,
|
||||
calling with an argument will set a new value.
|
||||
|
||||
Reading from the source from within any reactive scope will cause changes
|
||||
Reading from the source from within a reactive scope will cause changes
|
||||
to that source to be tracked and anything depending on it to update.
|
||||
|
||||
- **Example**
|
||||
|
|
@ -30,44 +54,39 @@ Creates a new source with the given value.
|
|||
count(count() + 1) -- 1
|
||||
```
|
||||
|
||||
## watch()
|
||||
## effect()
|
||||
|
||||
Runs a callback on source update.
|
||||
Runs a side-effect on source update.
|
||||
|
||||
- **Type**
|
||||
|
||||
```lua
|
||||
function watch(source: () -> ()): Unwatch
|
||||
|
||||
type Unwatch = () -> ()
|
||||
function effect(callback: () -> ())
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
The source callback is ran immediately to determine what states are
|
||||
referenced.
|
||||
The callback is ran immediately.
|
||||
|
||||
Any time a source referenced in the callback is changed, the callback will
|
||||
be reran.
|
||||
|
||||
Also returns a function that when called, stops the watcher immediately.
|
||||
|
||||
::: warning
|
||||
`source()` cannot yield.
|
||||
`callback()` cannot yield.
|
||||
:::
|
||||
|
||||
- **Example**
|
||||
|
||||
```lua
|
||||
local state = source(1)
|
||||
local num = source(1)
|
||||
|
||||
watch(function()
|
||||
print(state())
|
||||
effect(function()
|
||||
print(num())
|
||||
end)
|
||||
|
||||
-- prints 1
|
||||
|
||||
state(state() + 1)
|
||||
num(num() + 1)
|
||||
|
||||
-- prints 2
|
||||
```
|
||||
|
|
@ -110,150 +129,4 @@ Derives a new source from existing sources.
|
|||
text() -- "count: 1"
|
||||
```
|
||||
|
||||
## indexes()
|
||||
|
||||
Maps each index in a table source to an object.
|
||||
|
||||
- **Type**
|
||||
|
||||
```lua
|
||||
function indexes<KI, VI, VO>(
|
||||
source: () -> Map<KI, VI>,
|
||||
transform: (value: () -> VI, index: KI) -> VO
|
||||
): Array<VO>
|
||||
|
||||
- **Details**
|
||||
|
||||
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.
|
||||
|
||||
Anytime a new index is added, the transform function will be called again
|
||||
for that new index.
|
||||
|
||||
Anytime an existing index 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
|
||||
a UI element.
|
||||
|
||||
```lua
|
||||
type Item = {
|
||||
name: string,
|
||||
icon: number
|
||||
}
|
||||
|
||||
local items = source {} :: () -> Array<Item>
|
||||
|
||||
local displays = indexes(items, function(item, i)
|
||||
return ItemDisplay {
|
||||
Name = function()
|
||||
return item().name
|
||||
end,
|
||||
|
||||
Image = function()
|
||||
return "rbxassetid://" .. item().icon
|
||||
end,
|
||||
|
||||
LayoutOrder = i
|
||||
}
|
||||
end)
|
||||
```
|
||||
|
||||
## values()
|
||||
|
||||
Maps each value in a table source to an object.
|
||||
|
||||
- **Type**
|
||||
|
||||
```lua
|
||||
function values<KI, VI, VO>(
|
||||
source: () -> Map<KI, VI>,
|
||||
transform: (value: VI, index: () -> KI) -> VO
|
||||
): Array<VO>
|
||||
|
||||
- **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.
|
||||
|
||||
Anytime a new value is added, the transform function will be called again
|
||||
for that new value.
|
||||
|
||||
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.
|
||||
:::
|
||||
|
||||
- **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<Item>
|
||||
|
||||
local displays = values(items, function(item, i)
|
||||
return ItemDisplay {
|
||||
Name = item.Name
|
||||
|
||||
Image = "rbxassetid://" .. item.icon,
|
||||
|
||||
LayoutOrder = i
|
||||
}
|
||||
end)
|
||||
```
|
||||
|
||||
- **Extra**
|
||||
|
||||
When should you use `indexes()` and `values()`?
|
||||
|
||||
`values()` should be used when you have a fixed set of objects where the
|
||||
same objects can be re-arranged in the source table. It maps a value to a
|
||||
UI element.
|
||||
|
||||
e.g.
|
||||
- List of all players.
|
||||
- Inventory of items.
|
||||
- Chat message history.
|
||||
- Toast notifications.
|
||||
|
||||
`indexes()` should be used in other cases, especially when your source table
|
||||
has primitive value. It maps an index to a UI element.
|
||||
|
||||
e.g.
|
||||
- List of character or weapon stats.
|
||||
|
||||
In most cases, both functions will appear to have the same behavior.
|
||||
The main difference is performance, picking the right function to use can
|
||||
result in less property updates and less re-renders.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
|
|
|||
186
docs/api/reactivity-flow.md
Normal file
186
docs/api/reactivity-flow.md
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
# Reactivity API: Control Flow
|
||||
|
||||
<br/>
|
||||
|
||||
## switch()
|
||||
|
||||
Changes object based on a source and a mapping table.
|
||||
|
||||
- **Type**
|
||||
|
||||
```lua
|
||||
function switch<K, V>(source: () -> K): (map: Map<K, () -> V>) -> V?
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
The mapped function is ran in a new reactive scope that is destroyed when
|
||||
the source changes and maps to a different function.
|
||||
|
||||
::: warning
|
||||
Mapped functions cannot yield.
|
||||
:::
|
||||
|
||||
- **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<KI, VI, VO>(
|
||||
source: () -> Map<KI, VI>,
|
||||
transform: (value: () -> VI, index: KI) -> VO
|
||||
): Array<VO>
|
||||
|
||||
- **Details**
|
||||
|
||||
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.
|
||||
|
||||
Anytime a new index is added, the transform function will be called again
|
||||
for that new index.
|
||||
|
||||
Anytime an existing index 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
|
||||
a UI element.
|
||||
|
||||
```lua
|
||||
type Item = {
|
||||
name: string,
|
||||
icon: number
|
||||
}
|
||||
|
||||
local items = source {} :: () -> Array<Item>
|
||||
|
||||
local displays = indexes(items, function(item, i)
|
||||
return ItemDisplay {
|
||||
Name = function()
|
||||
return item().name
|
||||
end,
|
||||
|
||||
Image = function()
|
||||
return "rbxassetid://" .. item().icon
|
||||
end,
|
||||
|
||||
LayoutOrder = i
|
||||
}
|
||||
end)
|
||||
```
|
||||
|
||||
## values()
|
||||
|
||||
Maps each value in a table source to an object.
|
||||
|
||||
- **Type**
|
||||
|
||||
```lua
|
||||
function values<KI, VI, VO>(
|
||||
source: () -> Map<KI, VI>,
|
||||
transform: (value: VI, index: () -> KI) -> VO
|
||||
): Array<VO>
|
||||
|
||||
- **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.
|
||||
|
||||
Anytime a new value is added, the transform function will be called again
|
||||
for that new value.
|
||||
|
||||
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.
|
||||
:::
|
||||
|
||||
- **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<Item>
|
||||
|
||||
local displays = values(items, function(item, i)
|
||||
return ItemDisplay {
|
||||
Name = item.Name
|
||||
|
||||
Image = "rbxassetid://" .. item.icon,
|
||||
|
||||
LayoutOrder = i
|
||||
}
|
||||
end)
|
||||
```
|
||||
|
||||
- **Extra**
|
||||
|
||||
When should you use `indexes()` and `values()`?
|
||||
|
||||
`values()` should be used when you have a fixed set of objects where the
|
||||
same objects can be re-arranged in the source table. It maps a value to a
|
||||
UI element.
|
||||
|
||||
e.g.
|
||||
- List of all players.
|
||||
- Inventory of items.
|
||||
- Chat message history.
|
||||
- Toast notifications.
|
||||
|
||||
`indexes()` should be used in other cases, especially when your source table
|
||||
has primitive value. It maps an index to a UI element.
|
||||
|
||||
e.g.
|
||||
- List of character or weapon stats.
|
||||
|
||||
In most cases, both functions will appear to have the same behavior.
|
||||
The main difference is performance, picking the right function to use can
|
||||
result in less property updates and less re-renders.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## cleanup()
|
||||
|
||||
Runs a callback anytime a function scope is re-ran.
|
||||
Runs a callback anytime a reactive scope is re-ran.
|
||||
|
||||
- **Type**
|
||||
|
||||
|
|
@ -10,25 +10,12 @@ Runs a callback anytime a function scope is re-ran.
|
|||
function cleanup(callback: () -> ())
|
||||
```
|
||||
|
||||
- **Details**
|
||||
|
||||
The primary purpose of this function is to provide a means of cleaning up
|
||||
side effects caused by source updates and `watch()` updates.
|
||||
|
||||
The stack is inspected to find the function that calls `cleanup()`. The
|
||||
callback passed is called anytime the caller is re-ran, and when the caller
|
||||
finally garbage collects.
|
||||
|
||||
::: warning
|
||||
Only one `cleanup()` call is allowed per function scope.
|
||||
:::
|
||||
|
||||
- **Example**
|
||||
|
||||
```lua
|
||||
local data = source(1)
|
||||
|
||||
watch(function()
|
||||
effect(function()
|
||||
local label = create "TextLabel" { Text = data() }
|
||||
|
||||
cleanup(function()
|
||||
|
|
@ -53,7 +40,7 @@ Runs a callback anytime a function scope is re-ran.
|
|||
|
||||
## untrack()
|
||||
|
||||
Gets the value of a source without reactively tracking it.
|
||||
Runs a given function where any sources read will not track its reactive scope.
|
||||
|
||||
- **Type**
|
||||
|
||||
|
|
@ -82,3 +69,5 @@ Gets the value of a source without reactively tracking it.
|
|||
a(1)
|
||||
print(sum()) -- 2
|
||||
```
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -12,14 +12,14 @@ and identifying improper usage.
|
|||
Currently, strict mode will:
|
||||
|
||||
1. Run derived sources twice a source updates.
|
||||
2. Run watchers twice when 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 duplicate nested properties at same depth.
|
||||
6. Better error reporting and stack traces.
|
||||
7. Checks for multiple `cleanup()` calls in the same function scope.
|
||||
|
||||
By rerunning sources and watchers, any side-effects are made more apparent.
|
||||
By rerunning sources and effects, any side-effects are made more apparent.
|
||||
This also helps ensure that cleanups are being handled correctly.
|
||||
|
||||
Accidental yielding within reactive scopes can break Vide's reactive graph,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Table Source
|
||||
# Control Flow
|
||||
|
||||
Vide has specific functions for dealing with sources that store a table value.
|
||||
|
||||
66
docs/tut/control-flow/switch.md
Normal file
66
docs/tut/control-flow/switch.md
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# 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<Item>)
|
||||
|
||||
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.
|
||||
66
docs/tut/control-flow/values.md
Normal file
66
docs/tut/control-flow/values.md
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# 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<Item>)
|
||||
|
||||
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.
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
This is a brief tutorial designed to give you a quick run through the usage of
|
||||
Vide.
|
||||
|
||||
Vide is largely inspired by other UI libraries such as Solid and Fusion.
|
||||
Vide is heavily inspired by [Solid](https://www.solidjs.com/).
|
||||
|
||||
## Why Vide?
|
||||
|
||||
|
|
@ -19,7 +19,29 @@ Some of the main focuses behind Vide's design choices:
|
|||
- Reducing the amount of imports needed for usage by leveraging Luau's syntax
|
||||
and semantics.
|
||||
- Being completely typecheckable.
|
||||
- Flexibility, particularly with integrating other libraries and allowing users
|
||||
to use their own patterns.
|
||||
- A powerful reactive system that does not interfere with the lifetime of
|
||||
instances.
|
||||
- Flexibility with integrating other libraries and allowing users to use their
|
||||
own patterns.
|
||||
- Independence from instance lifetimes.
|
||||
- A powerful reactive system that can update specific properties as a result of
|
||||
state changes, updates are immediate with no diffing needed.
|
||||
|
||||
## Structure Of A Vide App
|
||||
|
||||
The entry point for all Vide apps is the `mount()` function. This function
|
||||
sets up Vide's reactivity system. It takes and calls a function that should
|
||||
create your entire app, and will apply its result to a target.
|
||||
|
||||
In Vide, your app should be composed of functions, each function creates a
|
||||
specific part of your app, and can be reused if needed. These functions are
|
||||
called *components*.
|
||||
|
||||
```lua
|
||||
|
||||
local function App()
|
||||
return create "ScreenGui" {
|
||||
create "TextLabel" { Text = "hi" }
|
||||
}
|
||||
end
|
||||
|
||||
mount(App, game.StarterGui)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,62 +1,54 @@
|
|||
# Creating UI Elements
|
||||
# Creating UI
|
||||
|
||||
Instances are created using [`create()`](../../api/creation.md#create).
|
||||
|
||||
```lua
|
||||
local vide = require(path_to_vide)
|
||||
local create = vide.create
|
||||
```
|
||||
Instances are created using `create()`.
|
||||
|
||||
`create()` returns a constructor for a class which then takes a table of
|
||||
properties to assign when creating a new instance for that class.
|
||||
|
||||
Luau allows us to omit parentheses `()` when calling functions with string or
|
||||
table literals for brevity.
|
||||
table literals which Vide takes advantage of for brevity.
|
||||
|
||||
```lua
|
||||
local frame = create "Frame" {
|
||||
Name = "Background",
|
||||
Position = UDim2.fromScale(0.5, 0.5)
|
||||
}
|
||||
```
|
||||
local vide = require(vide)
|
||||
local mount = vide.mount
|
||||
local create = vide.create
|
||||
|
||||
String keys are treated as properties and integer keys are treated as child
|
||||
instances.
|
||||
local function App()
|
||||
return create "ScreenGui" {
|
||||
create "Frame" {
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Position = UDim2.fromScale(0.5, 0.5),
|
||||
Size = UDim2.fromScale(0.4, 0.7),
|
||||
|
||||
```lua
|
||||
create "ScreenGui" {
|
||||
Parent = game.StarterGui,
|
||||
create "TextLabel" {
|
||||
Text = "hi"
|
||||
},
|
||||
|
||||
create "Frame" {
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Position = UDim2.fromScale(0.5, 0.5),
|
||||
Size = UDim2.fromScale(0.4, 0.7),
|
||||
create "TextLabel" {
|
||||
Text = "bye"
|
||||
},
|
||||
|
||||
create "TextLabel" {
|
||||
Text = "hi"
|
||||
},
|
||||
create "TextButton" {
|
||||
Text = "click me",
|
||||
|
||||
create"TextLabel" {
|
||||
Text = "bye"
|
||||
Activated = function()
|
||||
print "clicked!"
|
||||
end
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
mount(App, game.StarterGui)
|
||||
```
|
||||
|
||||
To connect to an event, just assign the event property a function.
|
||||
Assign a value to a string key to set a property, and assign a value to a
|
||||
number key to set a child. Events can be connected to by assigning a function
|
||||
to a string key.
|
||||
|
||||
All event arguments are passed into the function.
|
||||
|
||||
```lua
|
||||
create "TextButton" {
|
||||
Activated = function()
|
||||
print "clicked!"
|
||||
end
|
||||
}
|
||||
```
|
||||
|
||||
You can also use a form of aggregate initialization to create datatypes instead
|
||||
of explicitly typing out the class name and constructor.
|
||||
You can also use a shorthand to create datatypes instead of explicitly typing
|
||||
out the class name and constructor. The table will be unpacked into the `.new()`
|
||||
constructor of the property's type.
|
||||
|
||||
```lua
|
||||
create "Frame" {
|
||||
|
|
@ -64,7 +56,3 @@ create "Frame" {
|
|||
UDim2 = { 0.5, 0, 0.5, 0 }
|
||||
}
|
||||
```
|
||||
|
||||
When a property is assigned a table, Vide will inspect the type of the property
|
||||
being assigned to, and call that type's default `new()` constructor with the
|
||||
unpacked values from the assigned table.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ Components are custom-made reusable pieces of UI made from other pieces of UI.
|
|||
By using components you can make your application more modular and better
|
||||
organized.
|
||||
|
||||
```lua
|
||||
```lua [Button.luau]
|
||||
local vide = require(vide)
|
||||
local create = vide.create
|
||||
|
||||
local function Button(props: {
|
||||
Position: UDim2,
|
||||
Text: string,
|
||||
|
|
@ -20,29 +23,44 @@ local function Button(props: {
|
|||
Activated = props.Activated
|
||||
}
|
||||
end
|
||||
|
||||
return Button
|
||||
```
|
||||
|
||||
Above is a simple example of a button component with its background color set to
|
||||
a dark grey and with a fixed size.
|
||||
```lua [App.luau]
|
||||
local vide = require(vide)
|
||||
local mount = vide.mount
|
||||
local create = vide.create
|
||||
|
||||
local Button = require(Button)
|
||||
|
||||
local function App()
|
||||
return create "ScreenGui" {
|
||||
Button {
|
||||
Position = UDim2.fromOffset(200, 200),
|
||||
Text = "click me!",
|
||||
|
||||
Activated = function()
|
||||
print "clicked"
|
||||
end
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
mount(App, game.StarterGui)
|
||||
```
|
||||
|
||||
Above is a simple example of a button component with a set color and size,
|
||||
being reused across files.
|
||||
|
||||
A single parameter `props` is used to pass properties to the component.
|
||||
Creating instances of this button component is as simple as doing the below:
|
||||
|
||||
```lua
|
||||
local button = Button {
|
||||
Position = UDim2.new(),
|
||||
Text = "Click me!",
|
||||
|
||||
Activated = function()
|
||||
print "clicked"
|
||||
end
|
||||
}
|
||||
```
|
||||
|
||||
Components allow you to *encapsulate* behavior. You can only modify the
|
||||
component in ways that you allow in the component.
|
||||
|
||||
This also promotes code reusability. Anytime you want a new button all you do
|
||||
is call `Button {}` instead of creating and setting every property each time.
|
||||
When changing the button in future, any changes to the button file will be
|
||||
reflected anywhere the button is used throughout your app.
|
||||
|
||||
This can be extended to much more complicated UI.
|
||||
|
|
|
|||
|
|
@ -4,33 +4,18 @@
|
|||
core of reactivity in Vide, as updates to a source can automatically update
|
||||
properties or other sources depending on that source.
|
||||
|
||||
A source in Vide can be created using
|
||||
[`source()`](../../api/reactivity-core.md#source).
|
||||
A source in Vide can be created using `source()`.
|
||||
|
||||
```lua
|
||||
local vide = require(vide)
|
||||
local source = vide.source
|
||||
|
||||
local count = source(0)
|
||||
```
|
||||
|
||||
The value passed to `source()` is the initial value of the source.
|
||||
|
||||
The value of a source can be set by calling it with an argument, and can be read
|
||||
by calling it with no arguments.
|
||||
|
||||
```lua
|
||||
count(count() + 1) -- increment source by 1
|
||||
```
|
||||
|
||||
Below is an example of a stateful counter component.
|
||||
|
||||
```lua
|
||||
local function Counter(props: { Position: UDim2 })
|
||||
local function Counter()
|
||||
local count = source(0)
|
||||
|
||||
return create "TextButton" {
|
||||
Position = props.Position,
|
||||
Size = UDim2.new(200, 50),
|
||||
Position = UDim2.fromOffset(300, 300),
|
||||
Size = UDim2.fromOffset(200, 50),
|
||||
|
||||
Text = count,
|
||||
|
||||
|
|
@ -39,37 +24,28 @@ local function Counter(props: { Position: UDim2 })
|
|||
end
|
||||
}
|
||||
end
|
||||
|
||||
mount(function() return create "ScreenGui" { Counter {} } end, game.StarterGui)
|
||||
```
|
||||
|
||||
Each call of `Counter {}` will create a new counter element, each with their own
|
||||
independent count.
|
||||
The value passed to `source()` is the initial value of the source.
|
||||
|
||||
Vide detects when you assign a function to a property. This is known
|
||||
as *binding* and doing so will cause the property to *automatically* update
|
||||
whenever a source in that function is updated, by rerunning the function and
|
||||
assigning its return value. You can only bind non-event
|
||||
properties, otherwise the function is connected as the event callback.
|
||||
|
||||
This allows you as the programmer to not need to manually update GUI as the state
|
||||
of your program changes. You just define how the data maps to UI, and Vide's
|
||||
reactive system will surgically update any properties depending on sources that
|
||||
are changed.
|
||||
|
||||
Since sources are just functions, you can also pass external sources to
|
||||
components like so:
|
||||
The value of a source can be set by calling it with an argument, and can be read
|
||||
by calling it with no arguments.
|
||||
|
||||
```lua
|
||||
local function Text(p: {
|
||||
Text: () -> string
|
||||
})
|
||||
return create "TextLabel" {
|
||||
Text = p.Text
|
||||
}
|
||||
end
|
||||
|
||||
local text = source "hi"
|
||||
|
||||
Text {
|
||||
Text = text
|
||||
}
|
||||
count(count() + 1) -- increment count by 1
|
||||
```
|
||||
|
||||
Each call of `Counter {}` will create a new counter, each maintaining their
|
||||
own count.
|
||||
|
||||
When you assign a function to a non-event property, Vide will immediately run it
|
||||
and check what sources were read from. When updating those sources again after,
|
||||
this function will be re-ran and its return value applied to the property.
|
||||
This is known as *binding* properties.
|
||||
|
||||
This allows you as the programmer to not need to manually update UI as the state
|
||||
of your program changes. You just define how the data maps to UI, and Vide's
|
||||
reactive system will automatically update any properties depending on sources
|
||||
that are updated.
|
||||
|
|
|
|||
|
|
@ -1,102 +0,0 @@
|
|||
# Derived Source
|
||||
|
||||
You can create new sources from existing sources. This is known as *deriving
|
||||
sources*.
|
||||
|
||||
A function that wraps a source effectively becomes a new source. If a source
|
||||
used inside a function is updated, the whole function can be re-ran to recompute
|
||||
its value.
|
||||
|
||||
```lua
|
||||
local count = source(0)
|
||||
|
||||
local function text()
|
||||
return "count: " .. count()
|
||||
end
|
||||
|
||||
create "TextLabel" {
|
||||
Text = text
|
||||
}
|
||||
```
|
||||
|
||||
Sometimes when using expensive computations to derive state, you only want to
|
||||
recalculate it once when a source state has changed
|
||||
|
||||
If you wrap a source with a regular function, its value will be recomputed
|
||||
every time you call that function.
|
||||
[`derive()`](../../api/reactivity-core.md#derive) accepts a functions whose
|
||||
return value will be cached, so that subsequent calls of this derived source
|
||||
will return the same cached value until one of its input sources have changed.
|
||||
|
||||
```lua
|
||||
local derive = vide.derive
|
||||
```
|
||||
|
||||
```lua
|
||||
local count = source(0)
|
||||
|
||||
local factorial = derive(function()
|
||||
local n = 1
|
||||
for i = 2, count() do
|
||||
n *= i
|
||||
end
|
||||
return n
|
||||
end)
|
||||
```
|
||||
|
||||
This can improve performance in cases where a source is read from multiple times
|
||||
between recalculations, like in the example below:
|
||||
|
||||
```lua
|
||||
create "TextLabel" {
|
||||
Text = function()
|
||||
return "factorial squared: " .. factorial() * factorial()
|
||||
end
|
||||
}
|
||||
|
||||
count(3) -- displays "factorial squared: 36"
|
||||
count(4) -- displays "factorial squared: 576"
|
||||
```
|
||||
|
||||
Vide knows what sources are being depended on by immediately running the
|
||||
callback when deriving or binding sources. If a source is in a function but is
|
||||
never referenced the first time it runs, Vide will not know to rerun the
|
||||
function if that source changes.
|
||||
|
||||
An example to watch out for is when using sources within branches:
|
||||
|
||||
```lua
|
||||
local condition = source(true)
|
||||
local count1 = source(0)
|
||||
local count2 = source(0)
|
||||
|
||||
local text = function()
|
||||
if condition() then
|
||||
return "text: " .. count1()
|
||||
else
|
||||
return "text: " .. count2()
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
In the above case, only `count1` will be referenced, meaning `text` will not be
|
||||
aware of `count2` even if the condition is later set to false.
|
||||
|
||||
All sources to be tracked must be referenced the first time the function runs.
|
||||
|
||||
```lua
|
||||
local condition = source(true)
|
||||
local count1 = source(0)
|
||||
local count2 = source(0)
|
||||
|
||||
local text = function()
|
||||
local c1 = count1()
|
||||
local c2 = count2()
|
||||
|
||||
if condition() then
|
||||
return "text: " .. c1
|
||||
else
|
||||
return "text: " .. c2
|
||||
end
|
||||
end
|
||||
```
|
||||
45
docs/tut/crash-course/5-effect.md
Normal file
45
docs/tut/crash-course/5-effect.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# Effect
|
||||
|
||||
An effect is a function that is run anytime a source updates. They are called
|
||||
effects because they can produce side-effects when reacting to source changes.
|
||||
|
||||
Effects are created using `effect()`.
|
||||
|
||||
```lua
|
||||
local vide = require(vide)
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
|
||||
local function Counter()
|
||||
local count = source(0)
|
||||
|
||||
effect(function()
|
||||
print("count has updated to: " .. count())
|
||||
end)
|
||||
|
||||
return create "TextButton" {
|
||||
Position = UDim2.fromOffset(300, 300),
|
||||
Size = UDim2.fromOffset(200, 50),
|
||||
|
||||
Text = count,
|
||||
|
||||
Activated = function()
|
||||
count(count() + 1)
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
mount(function() return create "ScreenGui" { Counter {} } end, game.StarterGui)
|
||||
```
|
||||
|
||||
This will print to the terminal anytime the count is changed.
|
||||
|
||||
`effect()` creates an explicit side-effect. There are other side-effects in the
|
||||
above code sample. The setting of `Text = count` creates another side-effect;
|
||||
the updating of the Text property anytime the count is changed.
|
||||
|
||||
All observable changes to the user are considered to be side-effects of the
|
||||
reactive system.
|
||||
|
||||
You should not update other sources using an effect. Improper usage can lead to
|
||||
unecessary updates and infinite loops.
|
||||
76
docs/tut/crash-course/6-derived-source.md
Normal file
76
docs/tut/crash-course/6-derived-source.md
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
# Derived Source
|
||||
|
||||
You can create new sources from existing sources. This is known as *deriving
|
||||
sources*.
|
||||
|
||||
A function that wraps a source effectively becomes a new source. If a source
|
||||
used inside a function is updated, the whole function can be re-ran to recompute
|
||||
its value.
|
||||
|
||||
```lua
|
||||
local vide = require(vide)
|
||||
local source = vide.source
|
||||
|
||||
local function Counter()
|
||||
local count = source(0)
|
||||
|
||||
local function doubled()
|
||||
return count() * 2
|
||||
end
|
||||
|
||||
return create "TextButton" {
|
||||
Position = UDim2.fromOffset(300, 300),
|
||||
Size = UDim2.fromOffset(200, 50),
|
||||
|
||||
Text = doubled,
|
||||
|
||||
Activated = function()
|
||||
count(count() + 1)
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
mount(function() return create "ScreenGui" { Counter {} } end, game.StarterGui)
|
||||
```
|
||||
|
||||
Now the counter will increment in 2s each time it is clicked.
|
||||
|
||||
Sometimes when using expensive computations to derive state, you only want to
|
||||
recalculate it once when a source state has changed. Although not needed in
|
||||
most cases, you can use `derive()` to create a new source that will cache its
|
||||
value, only recomputing when an input source has changed.
|
||||
|
||||
```lua
|
||||
local vide = require(vide)
|
||||
local source = vide.source
|
||||
local derive = vide.derive
|
||||
|
||||
local function Counter()
|
||||
local count = source(0)
|
||||
|
||||
local factorial = derive(function()
|
||||
local n = 1
|
||||
for i = 2, count() do
|
||||
n *= i
|
||||
end
|
||||
return n
|
||||
end)
|
||||
|
||||
return create "TextButton" {
|
||||
Position = UDim2.fromOffset(300, 300),
|
||||
Size = UDim2.fromOffset(200, 50),
|
||||
|
||||
Text = function()
|
||||
return factorial() + factorial() + factorial()
|
||||
end,
|
||||
|
||||
Activated = function()
|
||||
count(count() + 1)
|
||||
end
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
This can improve performance in cases where a source is read from multiple times
|
||||
between recalculations. In the above example, the factorial is only ever
|
||||
calculated once each time the count changes.
|
||||
40
docs/tut/crash-course/7-cleanup.md
Normal file
40
docs/tut/crash-course/7-cleanup.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# Cleanup
|
||||
|
||||
Sometimes you may need to do some cleanup when destroying a component or after
|
||||
a side-effect from a source update. Vide provides a function `cleanup()` which
|
||||
is used to register a cleanup callback for the next time the reactive scope
|
||||
it is called in re-runs.
|
||||
|
||||
```lua
|
||||
local vide = require(vide)
|
||||
local source = vide.source
|
||||
local cleanup = vide.cleanup
|
||||
|
||||
local function Timer()
|
||||
local count = source(0)
|
||||
|
||||
local con = game:GetService("RunService").Heartbeat:Connect(function(dt)
|
||||
count(count() + dt)
|
||||
end)
|
||||
|
||||
cleanup(function()
|
||||
con:Disconnect()
|
||||
end)
|
||||
|
||||
return create "TextButton" {
|
||||
Position = UDim2.fromOffset(300, 300),
|
||||
Size = UDim2.fromOffset(200, 50),
|
||||
|
||||
Text = function()
|
||||
return "seconds: " .. count()
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
mount(function() return create "ScreenGui" { Timer {} } end, game.StarterGui)
|
||||
```
|
||||
|
||||
In the above example, this allows us to disconnect the heartbeat connection
|
||||
when the timer component is destroyed, whether that is from unmounting the app
|
||||
or if it is dynamically created by a control-flow function, which will be
|
||||
covered next.
|
||||
95
docs/tut/crash-course/8-control-flow.md
Normal file
95
docs/tut/crash-course/8-control-flow.md
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
# 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,
|
||||
known as *control flow* functions.
|
||||
|
||||
These functions return a new source, which holds 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.
|
||||
|
||||
```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 loggedIn = source(false)
|
||||
|
||||
local function LoginMenu()
|
||||
return Frame {
|
||||
switch(loggedIn) {
|
||||
[true] = function()
|
||||
return ToggleButton { Text = "Log out", Toggle = loggedIn }
|
||||
end,
|
||||
|
||||
[false] = function()
|
||||
return ToggleButton { Text = "Log in", Toggle = loggedIn }
|
||||
end
|
||||
}
|
||||
}
|
||||
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.
|
||||
|
||||
Another control flow function, `indexes()`, is used to create elements from an
|
||||
input table.
|
||||
|
||||
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, `indexes()` can autmatically run a transform function for each
|
||||
index and value, generating a UI element.
|
||||
|
||||
```lua
|
||||
local todoList = {
|
||||
"Finish the crash course",
|
||||
"Star vide's GitHub"
|
||||
}
|
||||
|
||||
local elements = indexes(todoList, function(todo, i)
|
||||
return create "TextLabel" {
|
||||
Text = function()
|
||||
return i .. ": " .. todo()
|
||||
end,
|
||||
|
||||
LayoutOrder = i
|
||||
}
|
||||
end)
|
||||
|
||||
mount(function()
|
||||
return create "ScreenGui" {
|
||||
create "UIListLayout" {}, elements
|
||||
}
|
||||
end, game.StarterGui)
|
||||
```
|
||||
|
||||
For each unique index in the passed table, the transform function will be called
|
||||
with 1. a source containing the value of the index, 2. the index itself.
|
||||
|
||||
When the value at an index is changed, the function is not reran. Instead, the
|
||||
given source is updated instead.
|
||||
|
||||
`indexes()` is said to map each *index* in a table to a UI element, each index
|
||||
has a single corresponding element.
|
||||
|
||||
An element is only destroyed if the value of an index is set to `nil`.
|
||||
61
docs/tut/reactive-scoping.md
Normal file
61
docs/tut/reactive-scoping.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# Reactive Scoping
|
||||
|
||||
This is a brief document designed to give the user more insight into how Vide's
|
||||
reactive graph works.
|
||||
|
||||
Each time you create and derive sources, a new node representing that source is
|
||||
created and added to the reactive graph. Each node stores a value and a
|
||||
side-effect function. Each node also keeps track of its parents and children,
|
||||
as well as any cleanups registered.
|
||||
|
||||
Any time a node is updated, Vide will traverse and update that node's children,
|
||||
its children's children, etc, until all nodes descending from that node has been
|
||||
updated. Traversal will stop at a node if that node's cached value does not
|
||||
change after an update.
|
||||
|
||||
For every node that is updated, a scope is opened for that node. These scopes
|
||||
are referred to as "reactive scopes". Any source read from within a node's scope
|
||||
will that node as a child. This is similar to cleanups, anytime a cleanup is
|
||||
registered, it is added to the node of the currently active scope.
|
||||
|
||||
The way Vide tracks reactive scopes, is by using a stack of nodes. The current
|
||||
active reactive scope is the node at the top of this stack.
|
||||
|
||||
When destroying a node, its descendents are traversed and also destroyed.
|
||||
When being destroyed, a node's connections (parents and children) are cleared,
|
||||
and any pending cleanup functions are ran.
|
||||
|
||||
The purpose of `root()` (which is called internally by `mount()`) is to setup
|
||||
the root node which will track any node created or derived inside its scope, or
|
||||
any cleanups registered. Without it, nodes could be garbage collected without a
|
||||
chance to run pending cleanups which can cause memory leakage.
|
||||
|
||||
Control flow functions in Vide are special, as they can dynamically create and
|
||||
destroy new root scopes.
|
||||
|
||||
It is the combination of the above which allows us to write components like so:
|
||||
|
||||
```lua
|
||||
local function Counter()
|
||||
local count = source(0)
|
||||
|
||||
local connection = stepped:Connect(function() count(count() + 1) end)
|
||||
|
||||
cleanup(function() connection:Disconnect() end)
|
||||
effect(function() print(count()) end)
|
||||
|
||||
return create "TextLabel" { Text = count }
|
||||
end
|
||||
```
|
||||
|
||||
Vide doesn't recognise this as a "component", that is a user abstraction. Vide
|
||||
just sees this as a function that creates nodes in the reactive graph.
|
||||
|
||||
Whenever the reactive scope that calls this function is destroyed, like by a
|
||||
control flow function, the registered cleanup will be called, and the effect
|
||||
(which is just a node on the reactive graph) is destroyed. The returned instance
|
||||
and the bound `count` source is just considered to be a side-effect, and with
|
||||
the reactive scope from which the side-effects stem from destroyed, the instance
|
||||
can be garbage collected - everything is nicely cleaned up.
|
||||
|
||||
> todo: add graphics
|
||||
Loading…
Add table
Add a link
Reference in a new issue