mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
Update docs
This commit is contained in:
parent
3aed45212a
commit
d682161c06
13 changed files with 315 additions and 493 deletions
188
docs/tut/advanced/nested-scoping.md
Normal file
188
docs/tut/advanced/nested-scoping.md
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
# Nested Reactive Scopes
|
||||
|
||||
Nesting reactive 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.
|
||||
|
||||
```lua
|
||||
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
|
||||
|
||||
mount(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 reactive scope 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()`:
|
||||
|
||||
```lua
|
||||
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 = mount(Button)
|
||||
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 `mount()` within the effect
|
||||
to manually create and destroy a new reactive scope whenever the effect reruns.
|
||||
|
||||
Alternatively, instead of using `mount()`, a new reactive scope can be created
|
||||
directly within the effect:
|
||||
|
||||
```lua
|
||||
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 that are tracking. 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.
|
||||
|
||||
|
||||
|
|
@ -1,253 +0,0 @@
|
|||
# Reactive Scoping
|
||||
|
||||
This is a brief document designed to give the user more insight into how Vide's
|
||||
reactive system works.
|
||||
|
||||
## Graph Basics
|
||||
|
||||
Vide's reactivity can be represented as a graph, where each source, derived
|
||||
source, and effect is a node on that graph. The term "*reactive scope*" is just
|
||||
an abstraction used to refer to these nodes. Each node is a reactive scope.
|
||||
|
||||
Each node stores a cached value, a side-effect function, cleanup functions,
|
||||
its parents and children, and its owner and owned.
|
||||
|
||||
Whenever a node is updated it will:
|
||||
|
||||
1. destroy its owned nodes
|
||||
2. run its cleanups
|
||||
3. rerun its side-effect and update its cached value
|
||||
4. if its cached value changes, update its children recursively.
|
||||
|
||||
There is a difference between children nodes and owned nodes:
|
||||
|
||||
- children nodes are updated when a parent is updated.
|
||||
- owned nodes are destroyed when a parent is updated.
|
||||
- both children and owned are destroyed when a parent is destroyed.
|
||||
|
||||
Nodes created by `root()` generally have no children, and only tracks owned.
|
||||
Nodes created by `derive()` generally have no owned, and only tracks children.
|
||||
|
||||
## Basic Example
|
||||
|
||||
```lua
|
||||
root(function()
|
||||
local forename = source "quan"
|
||||
local surname = source "xi"
|
||||
|
||||
local name = derive(function()
|
||||
return forename() .. " " .. surname()
|
||||
end)
|
||||
|
||||
effect(function()
|
||||
print("new name: " .. name())
|
||||
end)
|
||||
end)
|
||||
```
|
||||
|
||||
This code will produce a graph that looks like so:
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#161618"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph
|
||||
subgraph root
|
||||
forename & surname --> name
|
||||
name --> effect
|
||||
end
|
||||
```
|
||||
|
||||
Nodes connected by arrows represent parent and children connections.
|
||||
Nodes within other nodes represent owner and owned connections.
|
||||
|
||||
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.
|
||||
|
||||
When the side-effect for a node is being reran when a node is updated, any
|
||||
other nodes read within that side-effect are set as parents of the node
|
||||
currently being reran. As those nodes are read, we know that the current node
|
||||
depends on them, so any time those nodes are updated, they will update dependent
|
||||
nodes since they will be stored as children.
|
||||
|
||||
When destroying a node, its descendents are traversed and also destroyed.
|
||||
When being destroyed, a node's connections (parents and children, owner and
|
||||
owned) 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 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.
|
||||
|
||||
Nodes created by `source()` can actually exist outside of root nodes, since
|
||||
they do not have direct side-effects or cleanups, they do not have to be
|
||||
explicitly destroyed.
|
||||
|
||||
## Control-flow Graph Example
|
||||
|
||||
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(props: { text: string })
|
||||
local count = source(0)
|
||||
|
||||
local connection = stepped:Connect(function() count(count() + 1) end)
|
||||
|
||||
cleanup(function() connection:Disconnect() end)
|
||||
|
||||
return create "TextLabel" {
|
||||
Text = function()
|
||||
return props.text() .. ": " .. count()
|
||||
end
|
||||
}
|
||||
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.
|
||||
|
||||
```lua
|
||||
root(function()
|
||||
local counters = { "A", "B" }
|
||||
|
||||
indexes(counters, function(name)
|
||||
return Counter { text = name }
|
||||
end)
|
||||
end)
|
||||
```
|
||||
|
||||
This code produces a graph like so:
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#fff"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph LR
|
||||
subgraph root
|
||||
counters --> indexes
|
||||
|
||||
subgraph root1[subroot 1]
|
||||
n1[name] --> p1[prop binding]
|
||||
end
|
||||
|
||||
subgraph root2[subroot 2]
|
||||
n2[name] --> p2[prop binding]
|
||||
end
|
||||
end
|
||||
|
||||
indexes .-> root1 & root2
|
||||
```
|
||||
|
||||
This shows how the `indexes()` control flow function creates and manages new
|
||||
root scopes. The function creates an effect seen as `indexes` in the graph,
|
||||
which manages the new roots `subroot 1` and `subroot 2`, as well as the sources
|
||||
`name` for which one exists for each index value in the input table.
|
||||
|
||||
When the input table changes, `indexes()` can automatically destroy and create
|
||||
subroots based on the changed indexes. Destroyed nodes run any cleanups made, in
|
||||
this case it is the cleanups to disconnect the counters connection. The same
|
||||
applies to all other control flow functions.
|
||||
|
||||
Whenever the root reactive scope is destroyed, all its children, `counters` and
|
||||
`indexes` will be destroyed too, which means that `indexes` children, the
|
||||
subroots, will also be destroyed. Everything is nicely cleaned up.
|
||||
|
||||
## Custom Control-flow Example
|
||||
|
||||
Below is a simple example of the `show()` control-flow function.
|
||||
|
||||
Each time `visible` changes, `show()` will destroy the current reactive scope
|
||||
and rerun its function in a new one.
|
||||
|
||||
```lua
|
||||
local visible = source(true)
|
||||
local count = source(0)
|
||||
|
||||
root(function()
|
||||
show(visible, function()
|
||||
return create "TextLabel" { Text = count }
|
||||
end)
|
||||
end)
|
||||
```
|
||||
|
||||
The above code produces a graph like so:
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#1B1B1F"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph LR
|
||||
subgraph root
|
||||
direction LR
|
||||
show
|
||||
|
||||
subgraph subroot["show() subroot"]
|
||||
p1[prop binding]
|
||||
end
|
||||
end
|
||||
|
||||
visible --> show
|
||||
count --> p1
|
||||
show -.- subroot
|
||||
```
|
||||
|
||||
This can be recreated without the `show()` control-flow function, with the
|
||||
following code:
|
||||
|
||||
```lua
|
||||
local visible = source(true)
|
||||
local count = source(0)
|
||||
|
||||
root(function()
|
||||
local output = derive(function()
|
||||
visible()
|
||||
|
||||
-- untrack so any source read from within this scope
|
||||
-- will not cause the outer `derive()` call to rerun,
|
||||
-- we only want `derive()` to rerun when `visible` changes
|
||||
return untrack(function()
|
||||
local label = create "TextLabel" {}
|
||||
|
||||
effect(function()
|
||||
label.Text = count()
|
||||
end)
|
||||
|
||||
return label
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
```
|
||||
|
||||
Both of the above code samples will produce the same visible result.
|
||||
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
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 queue a cleanup callback for the next time a reactive scope re-runs.
|
||||
is used to queue a cleanup callback for the next time a reactive scope is rerun
|
||||
or destroyed.
|
||||
|
||||
```lua
|
||||
local mount = vide.mount
|
||||
|
|
@ -32,53 +33,19 @@ end
|
|||
|
||||
local unmount = mount(Timer)
|
||||
|
||||
unmount() -- all queued cleanups are ran, heartbeat connection stopped
|
||||
unmount() -- all queued cleanups are ran, heartbeat connection disconnected
|
||||
```
|
||||
|
||||
In the above example, this allows us to disconnect the heartbeat connection
|
||||
when the reactive scope responsible for creating the timer component is
|
||||
destroyed, such as when it is unmounted.
|
||||
|
||||
Vide does not see "components", it only sees reactive scopes and how they are
|
||||
linked together. Components are just a user pattern that creates UI instances
|
||||
alongside effects. In other words, instances are just a side-effect of the
|
||||
reactive graph. When a reactive scope is created, you create a corresponding
|
||||
instance to display that data, when that reactive scope is destroyed, any
|
||||
cleanups queued will be ran and take care of anything that needs to be, such
|
||||
as disconnecting connections.
|
||||
|
||||
This is another reason why `mount()` is used at the top level of your app, so
|
||||
that any registered cleanups created by your app components can be ran when
|
||||
they are destroyed.
|
||||
|
||||
Side note: Roblox instances do not need to be explicitly destroyed for their
|
||||
::: tip
|
||||
Roblox instances do not need to be explicitly destroyed for their
|
||||
memory to be freed, they only need to be parented to `nil`. So there is no
|
||||
need to use `cleanup()` to destroy instances. However, be wary of connecting
|
||||
a function that references an instance to an event from the same instance,
|
||||
this causes the instance to reference itself and never be freed. In such a case
|
||||
you would need to use `cleanup()` to disconnect this connection or to explicitly
|
||||
destroy the instance.
|
||||
|
||||
The reactive graph for the above example:
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#161618"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph
|
||||
|
||||
subgraph mount
|
||||
direction LR
|
||||
cleanup([cleanup]) ~~~ count
|
||||
count --> bind["effect (text binding)"]
|
||||
end
|
||||
```
|
||||
:::
|
||||
|
|
|
|||
|
|
@ -1,123 +1,57 @@
|
|||
# Control Flow
|
||||
|
||||
Eventually you will need a way to dynamically create and destroy UI elements
|
||||
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.
|
||||
These sources can be assigned as children, meaning the displayed children
|
||||
will update when the input source updates.
|
||||
|
||||
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.
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
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["mount() scope"]
|
||||
direction LR
|
||||
joined --> show -.- subroot
|
||||
|
||||
subgraph subroot["show() scope"]
|
||||
direction LR
|
||||
Button
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
`show()` will implicitly create an effect depending on `joined`, which can be
|
||||
seen as `show` on the graph. This effect manages, and can create or destroy
|
||||
a separate reactive scope seen as `show() scope` on the graph. The dotted line
|
||||
indicates that it isn't actually connected, only indirectly managed through
|
||||
code.
|
||||
Control flow functions run their components in a new reactive scope, which can
|
||||
be destroyed independently of the reactive scope that called the control flow
|
||||
function. This means parts of your app can be independently created and
|
||||
destroyed.
|
||||
|
||||
## 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.
|
||||
`switch()` condtionally displays one instance at a time. It uses a table to map
|
||||
a source value to a component.
|
||||
|
||||
```lua
|
||||
local source = vide.source
|
||||
local switch = vide.switch
|
||||
|
||||
local function Button(props: {
|
||||
Text: string,
|
||||
Activated: () -> ()
|
||||
})
|
||||
local hovered = source(false)
|
||||
|
||||
return create "TextButton" {
|
||||
Text = props.Text,
|
||||
Activated = props.Activated,
|
||||
|
||||
TextColor3 = function()
|
||||
return hovered() and Color3.new(1, 1, 1) or Color3.new(.7, .7, .7)
|
||||
end,
|
||||
|
||||
MouseEnter = function() hovered(true) end,
|
||||
MouseLeave = function() hovered(false) end
|
||||
}
|
||||
end
|
||||
|
||||
local function JoinMenu()
|
||||
local joined = source(false)
|
||||
|
||||
local function JoinButton()
|
||||
return Button {
|
||||
Text = "Join",
|
||||
Activated = function() joined(true) end
|
||||
}
|
||||
end
|
||||
|
||||
local function LeaveButton()
|
||||
return Button {
|
||||
Text = "Leave"
|
||||
Activated = function() joined(false) end
|
||||
}
|
||||
end
|
||||
|
|
@ -131,22 +65,6 @@ local function JoinMenu()
|
|||
end
|
||||
```
|
||||
|
||||
This example is equivalent to the previous one.
|
||||
|
||||
The switch can map any value to any component.
|
||||
|
||||
```lua
|
||||
type ActiveMenu = "none" | "inventory" | "shop" | "settings"
|
||||
|
||||
local menu = source "inventory"
|
||||
|
||||
switch(menu) {
|
||||
inventory = InventoryMenu,
|
||||
shop = ShopMenu,
|
||||
settings = SettingsMenu
|
||||
}
|
||||
```
|
||||
|
||||
The reactive graph for the above example:
|
||||
|
||||
```mermaid
|
||||
|
|
@ -164,23 +82,30 @@ The reactive graph for the above example:
|
|||
|
||||
graph
|
||||
|
||||
subgraph root["mount() scope"]
|
||||
subgraph root["root scope"]
|
||||
direction LR
|
||||
menu --> switch -.- subroot
|
||||
joined --> switch -.- subroot
|
||||
|
||||
subgraph subroot["switch() scope"]
|
||||
subgraph subroot["switch scope"]
|
||||
direction LR
|
||||
Menu
|
||||
effect["TextColor3 effect"]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
A `switch()` call creates a new effect and a new scope as seen in the above
|
||||
graph. Whenever `menu` updates, it causes the `switch` effect to run, which
|
||||
will destroy and recreate the switch scope with the new component.
|
||||
|
||||
This will also destroy the internal effect that the button uses to highlight
|
||||
itself when it is hovered, each time the switch is rerun.
|
||||
|
||||
## indexes()
|
||||
|
||||
Often, you will have a table of values with each value displayed in a similar
|
||||
manner. Rather than manually looping over each value to generate a corresponding
|
||||
UI element, `indexes()` allows you to create elements for each table index, to
|
||||
display the value at that index.
|
||||
UI element, `indexes()` allows you to create elements each corresponding to a
|
||||
table index, to display the value at that index.
|
||||
|
||||
```lua
|
||||
local todoList = source {
|
||||
|
|
@ -222,7 +147,8 @@ value), will have its corresponding reactive scope destroyed to clean up that
|
|||
element.
|
||||
|
||||
`indexes()` is said to *map* each table index to a new UI element that can
|
||||
update to display the current value at that index.
|
||||
update to display the current value at that index. Each table index is given a
|
||||
single corresponding UI element.
|
||||
|
||||
The reactive graph for the above example:
|
||||
|
||||
|
|
@ -241,16 +167,16 @@ The reactive graph for the above example:
|
|||
|
||||
graph
|
||||
|
||||
subgraph root ["mount() scope"]
|
||||
subgraph root ["root scope"]
|
||||
direction LR
|
||||
todoList --> indexes -.- subroot1 & subroot2
|
||||
|
||||
subgraph subroot1 ["indexes() scope 1"]
|
||||
subgraph subroot1 ["indexes scope 1"]
|
||||
direction LR
|
||||
value1[todo] --> prop1["prop binding"]
|
||||
end
|
||||
|
||||
subgraph subroot2 ["indexes() scope 2"]
|
||||
subgraph subroot2 ["indexes scope 2"]
|
||||
direction LR
|
||||
value2[todo] --> prop2[prop binding]
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,73 +6,64 @@ A summary of all the concepts covered during the crash course.
|
|||
|
||||
A source of data.
|
||||
|
||||
Stores a single value that can be updated by the user.
|
||||
Stores a single value that can be updated.
|
||||
|
||||
Created with `source()`.
|
||||
|
||||
# Derived Source
|
||||
|
||||
A new source composed of other sources.
|
||||
|
||||
Created with a plain function or with `derive()`.
|
||||
|
||||
## Effect
|
||||
|
||||
Anything that happens in reponse to a source update.
|
||||
Anything that happens in response to a source update.
|
||||
|
||||
Vide has built-in functions to create effects such as
|
||||
|
||||
- `effect()` - runs arbitrary user code on source update
|
||||
- `derive()` - updates a derived source on source update
|
||||
Created with `effect()`.
|
||||
|
||||
## Reactive Scope
|
||||
|
||||
A scope created by certain Vide functions where source updates can be tracked,
|
||||
and cleanups queued.
|
||||
|
||||
When a source used inside a reactive scope is updated, the reactive scope will
|
||||
rerun.
|
||||
|
||||
Reactive scopes are created by functions such as
|
||||
A scope created by certain functions such as:
|
||||
|
||||
- `root()`
|
||||
- `effect()`
|
||||
- `derive()`
|
||||
|
||||
## Owner
|
||||
Reactive scopes can:
|
||||
|
||||
A reactive scope created within an outer reactive scope, is *owned* by the outer
|
||||
reactive scope.
|
||||
- track sources that are read from within.
|
||||
- rerun when a tracked source updates.
|
||||
- track new reactive scopes created from within.
|
||||
|
||||
When a reactive scope is re-ran or destroyed, all reactive scopes owned by it
|
||||
are also destroyed.
|
||||
## Scope Owners
|
||||
|
||||
Vide does not let you create reactive scopes without owners.
|
||||
A reactive scope created within another reactive scope is *owned* by the other
|
||||
reactive scope, with the exception of the reactive scope created by `root()`.
|
||||
|
||||
## Root Reactive Scope
|
||||
When a reactive scope is rerun or destroyed, all reactive scopes owned by it are
|
||||
automatically destroyed.
|
||||
|
||||
A top-level reactive scope. These scopes are an exception to the owner rule.
|
||||
|
||||
Created by `root()`, which `mount()` uses internally.
|
||||
|
||||
A root reactive scope can be created on its own. It allows other reactive scopes
|
||||
to be created with an owner.
|
||||
|
||||
Root reactive scopes must be destroyed manually by the user, a function to do
|
||||
this is given by `root()`.
|
||||
|
||||
A root reactive scope can be created within another reactive scope and it will
|
||||
not automatically be owned by that scope.
|
||||
`root()`, which `mount()` uses internally, creates a reactive scope with no
|
||||
owner, since it must be destroyed manually using a destructor
|
||||
returned.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Cleans up the result from an effect.
|
||||
Arbitrary code to run whenever a reactive scope is rerun or destroyed.
|
||||
|
||||
Unneeded in most cases, a cleanup is arbitrary code that can be ran before
|
||||
a reactive scope is rerun or destroyed, so that the result from the previous
|
||||
run can be cleaned up. A cleanup can be queued by using `cleanup()` within
|
||||
a reactive scope.
|
||||
Queue a function to run using `cleanup()`.
|
||||
|
||||
## Tracking
|
||||
|
||||
Reactive scopes are tracking by default, meaning sources read from within scope
|
||||
will be tracked.
|
||||
Sources read from within a reactive scope will be tracked. This can be disabled
|
||||
using `untrack()`, which will make reactive scopes temporarily ignore sources
|
||||
read.
|
||||
|
||||
A reactive scope can be made temporarily non-tracking within `untrack()`, so
|
||||
that any source used will be ignored. The only function that creates a
|
||||
nontracking reactive scope by default is `root()`.
|
||||
The reactive scope created by `root()` is non-tracking by default.
|
||||
|
||||
As a guard against misusage, a reactive scope cannot be created within a
|
||||
reactive scope, unless it is made non-tracking using `untrack()`.
|
||||
|
||||
## Reactive Graph
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Instances are created using `create()`.
|
|||
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 which Vide takes advantage of for brevity.
|
||||
table literals which is recommended to use for brevity.
|
||||
|
||||
```lua
|
||||
local create = vide.create
|
||||
|
|
@ -44,6 +44,5 @@ to a string key.
|
|||
When creating an instance with no properties, it is important to not forget to
|
||||
actually call the constructor: `create "Frame" {}` and not `create "Frame"`.
|
||||
To be clear, `create "Frame"` returns a *function* which is a constructor for
|
||||
that class, not an instance of that class. This would result in you attempting
|
||||
to parent a function instead of an instance which is not correct.
|
||||
that class, not an instance of that class.
|
||||
:::
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ return Button
|
|||
```
|
||||
|
||||
```lua [App.luau]
|
||||
local mount = vide.mount
|
||||
local create = vide.create
|
||||
|
||||
local Button = require(Button)
|
||||
|
|
@ -60,7 +59,7 @@ local function App()
|
|||
}
|
||||
end
|
||||
|
||||
mount(App, game.StarterGui)
|
||||
App().Parent = game.StarterGui
|
||||
```
|
||||
|
||||
:::
|
||||
|
|
@ -76,8 +75,3 @@ To create a new button all you must do is call the `Button` function, passing in
|
|||
values. This saves having to create and set every property each time. Also, when
|
||||
updating the button component in future, any changes to the button file will be
|
||||
seen anywhere the button is used in your app.
|
||||
|
||||
The `mount()` function is used to set up Vide's reactivity system when creating
|
||||
your UI. It only needs to be called once at the top-level with the function that
|
||||
puts together your entire app. It also parents the returned instance to another
|
||||
a target instance for you.
|
||||
|
|
|
|||
|
|
@ -21,11 +21,11 @@ count(1)
|
|||
-- "count: 1" printed
|
||||
```
|
||||
|
||||
The callback given to `effect()` is initially ran immediately in a
|
||||
*reactive scope*. Any source read from inside a reactive scope will be tracked,
|
||||
so that if any of those sources update, the effect will be reran too.
|
||||
The callback given to `effect()` is ran immediately in a *reactive scope*. Any
|
||||
source read from inside a reactive scope will be tracked, so when any of those
|
||||
sources update, the effect will be reran too.
|
||||
|
||||
Effects also work with derived sources, it doesn't matter how deeply nested
|
||||
Reactive scopes also track derived sources, it doesn't matter how deeply nested
|
||||
inside a function a source is.
|
||||
|
||||
```lua
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
# Root Reactive Scopes
|
||||
|
||||
Any reactive scopes created, such as by `effect()`, must be done so within a
|
||||
"root" reactive scope. This is the main purpose of `mount()`, which you use
|
||||
once at the top level to create your UI.
|
||||
Reactive scopes cannot be created on their own - they must be created within
|
||||
another reactive scope so that it can be tracked and later destroyed when it is
|
||||
no longer needed.
|
||||
|
||||
This is so that if you want to destroy your UI, it can stop any reactive scopes
|
||||
created within it, since reactive scopes track any reactive scopes created
|
||||
within them.
|
||||
This is the purpose of `mount()`, which creates an initial "root", or
|
||||
"top-level" reactive scope, which all other reactive scopes, such as
|
||||
ones created by `effect()`, can stem from.
|
||||
|
||||
When this root reactive scope is destroyed, it will ensure all other reactive
|
||||
scopes created within it are also destroyed, ensuring everything is cleaned up
|
||||
properly.
|
||||
|
||||
```lua
|
||||
local source = vide.source
|
||||
|
|
@ -20,13 +24,15 @@ local function App()
|
|||
end)
|
||||
end
|
||||
|
||||
vide.mount(App) -- works!
|
||||
|
||||
App() -- will error since effect() was not called within a reactive scope
|
||||
|
||||
vide.mount(App) -- works!
|
||||
|
||||
```
|
||||
|
||||
Mounting returns a function that when called will destroy any reactive scopes
|
||||
created during the `mount()` call.
|
||||
Mounting returns a function that when called will destroy its reactive scope,
|
||||
along with any other reactive scopes created inside it.
|
||||
|
||||
```lua
|
||||
local unmount = mount(App)
|
||||
|
|
@ -68,7 +74,7 @@ memory. The effect being destroyed will remove this reference, allowing the
|
|||
instance to be garbage collected.
|
||||
|
||||
You don't need to worry about ensuring all your effects are created within a
|
||||
root scope, since you should be creating all your UI and corresponding effects
|
||||
within a top-level `mount()` call that puts all your UI together. So it is safe
|
||||
to assume that any effect you create will be created under this top level scope.
|
||||
Vide will prevent you from accidently doing otherwise anyways.
|
||||
root reactive scope, since you should be creating all your UI and corresponding
|
||||
effects within a top-level `mount()` call that puts all your UI together. So it
|
||||
is safe to assume that any effect you create will be created under this top
|
||||
level scope. Vide will prevent you from accidently doing otherwise anyways.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Stateful Components
|
||||
|
||||
A stateful component is a component that can update in reponse to data.
|
||||
A stateful component is a component that stores some data internally.
|
||||
|
||||
Stateful components in Vide are created using sources and effects - sources to
|
||||
store the data, and effects to display the data.
|
||||
|
|
@ -27,13 +27,18 @@ local function Counter()
|
|||
|
||||
return instance
|
||||
end
|
||||
|
||||
mount(Counter, game.StarterGui)
|
||||
```
|
||||
|
||||
Above is an example of a counter component, that when clicked, will increment
|
||||
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 scope of the component.
|
||||
count source is created inside the component.
|
||||
|
||||
We use `mount()` to create the counter within a reactive scope, which also takes
|
||||
a second argument to parent the counter to another instance.
|
||||
|
||||
## External State
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
# Property Binding
|
||||
|
||||
Explicitly creating effects to update properties can become verbose when there
|
||||
are a lot of properties to update. Vide provides a way to *implicitly* create
|
||||
an effect to update properties on source update.
|
||||
Explicitly creating effects to update properties can be tedious. Vide provides a
|
||||
way to *implicitly* create an effect to update properties.
|
||||
|
||||
```lua
|
||||
local create = vide.create
|
||||
|
|
@ -12,12 +11,12 @@ local function Counter()
|
|||
local count = source(0)
|
||||
|
||||
return create "TextButton" {
|
||||
Text = function()
|
||||
return "count: " .. count()
|
||||
end,
|
||||
|
||||
Activated = function()
|
||||
count(count() + 1)
|
||||
end,
|
||||
|
||||
Text = function()
|
||||
return "count: " .. count()
|
||||
end
|
||||
}
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue