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
|
|
@ -58,7 +58,7 @@ export default withMermaid({
|
||||||
{
|
{
|
||||||
text: "Advanced Reactivity",
|
text: "Advanced Reactivity",
|
||||||
items: [
|
items: [
|
||||||
{ text: "Reactive Scopes", link: "/tut/advanced/reactive-scoping.md"}
|
{ text: "Nested Scopes", link: "/tut/advanced/nested-scoping.md"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
||||||
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
|
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
|
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
|
```lua
|
||||||
local mount = vide.mount
|
local mount = vide.mount
|
||||||
|
|
@ -32,53 +33,19 @@ end
|
||||||
|
|
||||||
local unmount = mount(Timer)
|
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
|
In the above example, this allows us to disconnect the heartbeat connection
|
||||||
when the reactive scope responsible for creating the timer component is
|
when the reactive scope responsible for creating the timer component is
|
||||||
destroyed, such as when it is unmounted.
|
destroyed, such as when it is unmounted.
|
||||||
|
|
||||||
Vide does not see "components", it only sees reactive scopes and how they are
|
::: tip
|
||||||
linked together. Components are just a user pattern that creates UI instances
|
Roblox instances do not need to be explicitly destroyed for their
|
||||||
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
|
|
||||||
memory to be freed, they only need to be parented to `nil`. So there is no
|
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
|
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,
|
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
|
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
|
you would need to use `cleanup()` to disconnect this connection or to explicitly
|
||||||
destroy the instance.
|
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
|
# 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,
|
resulting from source updates. Vide provides functions to help you do this,
|
||||||
known as *control flow* functions.
|
known as *control flow* functions.
|
||||||
|
|
||||||
These functions return new sources, which hold 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.
|
|
||||||
|
|
||||||
Control flow functions are special, because they run their components in a new
|
Control flow functions run their components in a new reactive scope, which can
|
||||||
reactive scope, which can be destroyed independently of the reactive scope that
|
be destroyed independently of the reactive scope that called the control flow
|
||||||
called the control flow function itself. This means that parts of your app can
|
function. This means parts of your app can be independently created and
|
||||||
be independently created then destroyed.
|
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.
|
|
||||||
|
|
||||||
## switch()
|
## switch()
|
||||||
|
|
||||||
Similar to `show()`, `switch()`, also condtionally displays one instance at a
|
`switch()` condtionally displays one instance at a time. It uses a table to map
|
||||||
time. It is more flexible since it can show one of many components, based on a
|
a source value to a component.
|
||||||
table used to map a source value to a component.
|
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
local switch = vide.switch
|
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 function JoinMenu()
|
||||||
local joined = source(false)
|
local joined = source(false)
|
||||||
|
|
||||||
local function JoinButton()
|
local function JoinButton()
|
||||||
return Button {
|
return Button {
|
||||||
|
Text = "Join",
|
||||||
Activated = function() joined(true) end
|
Activated = function() joined(true) end
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
local function LeaveButton()
|
local function LeaveButton()
|
||||||
return Button {
|
return Button {
|
||||||
|
Text = "Leave"
|
||||||
Activated = function() joined(false) end
|
Activated = function() joined(false) end
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
@ -131,22 +65,6 @@ local function JoinMenu()
|
||||||
end
|
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:
|
The reactive graph for the above example:
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
|
|
@ -164,23 +82,30 @@ The reactive graph for the above example:
|
||||||
|
|
||||||
graph
|
graph
|
||||||
|
|
||||||
subgraph root["mount() scope"]
|
subgraph root["root scope"]
|
||||||
direction LR
|
direction LR
|
||||||
menu --> switch -.- subroot
|
joined --> switch -.- subroot
|
||||||
|
|
||||||
subgraph subroot["switch() scope"]
|
subgraph subroot["switch scope"]
|
||||||
direction LR
|
direction LR
|
||||||
Menu
|
effect["TextColor3 effect"]
|
||||||
end
|
end
|
||||||
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()
|
## indexes()
|
||||||
|
|
||||||
Often, you will have a table of values with each value displayed in a similar
|
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
|
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
|
UI element, `indexes()` allows you to create elements each corresponding to a
|
||||||
display the value at that index.
|
table index, to display the value at that index.
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local todoList = source {
|
local todoList = source {
|
||||||
|
|
@ -222,7 +147,8 @@ value), will have its corresponding reactive scope destroyed to clean up that
|
||||||
element.
|
element.
|
||||||
|
|
||||||
`indexes()` is said to *map* each table index to a new UI element that can
|
`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:
|
The reactive graph for the above example:
|
||||||
|
|
||||||
|
|
@ -241,16 +167,16 @@ The reactive graph for the above example:
|
||||||
|
|
||||||
graph
|
graph
|
||||||
|
|
||||||
subgraph root ["mount() scope"]
|
subgraph root ["root scope"]
|
||||||
direction LR
|
direction LR
|
||||||
todoList --> indexes -.- subroot1 & subroot2
|
todoList --> indexes -.- subroot1 & subroot2
|
||||||
|
|
||||||
subgraph subroot1 ["indexes() scope 1"]
|
subgraph subroot1 ["indexes scope 1"]
|
||||||
direction LR
|
direction LR
|
||||||
value1[todo] --> prop1["prop binding"]
|
value1[todo] --> prop1["prop binding"]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph subroot2 ["indexes() scope 2"]
|
subgraph subroot2 ["indexes scope 2"]
|
||||||
direction LR
|
direction LR
|
||||||
value2[todo] --> prop2[prop binding]
|
value2[todo] --> prop2[prop binding]
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -6,73 +6,64 @@ A summary of all the concepts covered during the crash course.
|
||||||
|
|
||||||
A source of data.
|
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
|
## 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
|
Created with `effect()`.
|
||||||
|
|
||||||
- `effect()` - runs arbitrary user code on source update
|
|
||||||
- `derive()` - updates a derived source on source update
|
|
||||||
|
|
||||||
## Reactive Scope
|
## Reactive Scope
|
||||||
|
|
||||||
A scope created by certain Vide functions where source updates can be tracked,
|
A scope created by certain functions such as:
|
||||||
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
|
|
||||||
|
|
||||||
- `root()`
|
- `root()`
|
||||||
- `effect()`
|
- `effect()`
|
||||||
- `derive()`
|
- `derive()`
|
||||||
|
|
||||||
## Owner
|
Reactive scopes can:
|
||||||
|
|
||||||
A reactive scope created within an outer reactive scope, is *owned* by the outer
|
- track sources that are read from within.
|
||||||
reactive scope.
|
- 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
|
## Scope Owners
|
||||||
are also destroyed.
|
|
||||||
|
|
||||||
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.
|
`root()`, which `mount()` uses internally, creates a reactive scope with no
|
||||||
|
owner, since it must be destroyed manually using a destructor
|
||||||
Created by `root()`, which `mount()` uses internally.
|
returned.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Cleanup
|
## 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
|
Queue a function to run using `cleanup()`.
|
||||||
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.
|
|
||||||
|
|
||||||
## Tracking
|
## Tracking
|
||||||
|
|
||||||
Reactive scopes are tracking by default, meaning sources read from within scope
|
Sources read from within a reactive scope will be tracked. This can be disabled
|
||||||
will be tracked.
|
using `untrack()`, which will make reactive scopes temporarily ignore sources
|
||||||
|
read.
|
||||||
|
|
||||||
A reactive scope can be made temporarily non-tracking within `untrack()`, so
|
The reactive scope created by `root()` is non-tracking by default.
|
||||||
that any source used will be ignored. The only function that creates a
|
|
||||||
nontracking reactive scope by default is `root()`.
|
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
|
## Reactive Graph
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ Instances are created using `create()`.
|
||||||
properties to assign when creating a new instance for that class.
|
properties to assign when creating a new instance for that class.
|
||||||
|
|
||||||
Luau allows us to omit parentheses `()` when calling functions with string or
|
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
|
```lua
|
||||||
local create = vide.create
|
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
|
When creating an instance with no properties, it is important to not forget to
|
||||||
actually call the constructor: `create "Frame" {}` and not `create "Frame"`.
|
actually call the constructor: `create "Frame" {}` and not `create "Frame"`.
|
||||||
To be clear, `create "Frame"` returns a *function* which is a constructor for
|
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
|
that class, not an instance of that class.
|
||||||
to parent a function instead of an instance which is not correct.
|
|
||||||
:::
|
:::
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ return Button
|
||||||
```
|
```
|
||||||
|
|
||||||
```lua [App.luau]
|
```lua [App.luau]
|
||||||
local mount = vide.mount
|
|
||||||
local create = vide.create
|
local create = vide.create
|
||||||
|
|
||||||
local Button = require(Button)
|
local Button = require(Button)
|
||||||
|
|
@ -60,7 +59,7 @@ local function App()
|
||||||
}
|
}
|
||||||
end
|
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
|
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
|
updating the button component in future, any changes to the button file will be
|
||||||
seen anywhere the button is used in your app.
|
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
|
-- "count: 1" printed
|
||||||
```
|
```
|
||||||
|
|
||||||
The callback given to `effect()` is initially ran immediately in a
|
The callback given to `effect()` is ran immediately in a *reactive scope*. Any
|
||||||
*reactive scope*. Any source read from inside a reactive scope will be tracked,
|
source read from inside a reactive scope will be tracked, so when any of those
|
||||||
so that if any of those sources update, the effect will be reran too.
|
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.
|
inside a function a source is.
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
# Root Reactive Scopes
|
# Root Reactive Scopes
|
||||||
|
|
||||||
Any reactive scopes created, such as by `effect()`, must be done so within a
|
Reactive scopes cannot be created on their own - they must be created within
|
||||||
"root" reactive scope. This is the main purpose of `mount()`, which you use
|
another reactive scope so that it can be tracked and later destroyed when it is
|
||||||
once at the top level to create your UI.
|
no longer needed.
|
||||||
|
|
||||||
This is so that if you want to destroy your UI, it can stop any reactive scopes
|
This is the purpose of `mount()`, which creates an initial "root", or
|
||||||
created within it, since reactive scopes track any reactive scopes created
|
"top-level" reactive scope, which all other reactive scopes, such as
|
||||||
within them.
|
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
|
```lua
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
|
|
@ -20,13 +24,15 @@ local function App()
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
vide.mount(App) -- works!
|
|
||||||
|
|
||||||
App() -- will error since effect() was not called within a reactive scope
|
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
|
Mounting returns a function that when called will destroy its reactive scope,
|
||||||
created during the `mount()` call.
|
along with any other reactive scopes created inside it.
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local unmount = mount(App)
|
local unmount = mount(App)
|
||||||
|
|
@ -68,7 +74,7 @@ memory. The effect being destroyed will remove this reference, allowing the
|
||||||
instance to be garbage collected.
|
instance to be garbage collected.
|
||||||
|
|
||||||
You don't need to worry about ensuring all your effects are created within a
|
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
|
root reactive scope, since you should be creating all your UI and corresponding
|
||||||
within a top-level `mount()` call that puts all your UI together. So it is safe
|
effects within a top-level `mount()` call that puts all your UI together. So it
|
||||||
to assume that any effect you create will be created under this top level scope.
|
is safe to assume that any effect you create will be created under this top
|
||||||
Vide will prevent you from accidently doing otherwise anyways.
|
level scope. Vide will prevent you from accidently doing otherwise anyways.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Stateful Components
|
# 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
|
Stateful components in Vide are created using sources and effects - sources to
|
||||||
store the data, and effects to display the data.
|
store the data, and effects to display the data.
|
||||||
|
|
@ -27,13 +27,18 @@ local function Counter()
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
end
|
end
|
||||||
|
|
||||||
|
mount(Counter, game.StarterGui)
|
||||||
```
|
```
|
||||||
|
|
||||||
Above is an example of a counter component, that when clicked, will increment
|
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.
|
its internal count, and automatically update its text to reflect that count.
|
||||||
|
|
||||||
Each instance of `Counter()` will maintain its own independent count, since the
|
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
|
## External State
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
# Property Binding
|
# Property Binding
|
||||||
|
|
||||||
Explicitly creating effects to update properties can become verbose when there
|
Explicitly creating effects to update properties can be tedious. Vide provides a
|
||||||
are a lot of properties to update. Vide provides a way to *implicitly* create
|
way to *implicitly* create an effect to update properties.
|
||||||
an effect to update properties on source update.
|
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local create = vide.create
|
local create = vide.create
|
||||||
|
|
@ -12,12 +11,12 @@ local function Counter()
|
||||||
local count = source(0)
|
local count = source(0)
|
||||||
|
|
||||||
return create "TextButton" {
|
return create "TextButton" {
|
||||||
Text = function()
|
|
||||||
return "count: " .. count()
|
|
||||||
end,
|
|
||||||
|
|
||||||
Activated = function()
|
Activated = function()
|
||||||
count(count() + 1)
|
count(count() + 1)
|
||||||
|
end,
|
||||||
|
|
||||||
|
Text = function()
|
||||||
|
return "count: " .. count()
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -45,9 +45,9 @@ local function assert_owning_scope(): Node<unknown>
|
||||||
|
|
||||||
if not scope then
|
if not scope then
|
||||||
local caller_name = debug.info(2, "n")
|
local caller_name = debug.info(2, "n")
|
||||||
return throw(`cannot use {caller_name}() in non-reactive scope, must be used within a root() or mount() callback`)
|
return throw(`cannot use {caller_name}() in a non-reactive scope`)
|
||||||
elseif scope.effect then
|
elseif scope.effect then
|
||||||
throw("cannot create new reactive scope inside of a tracking scope") -- todo: allow this?
|
throw("cannot create new reactive scope in a tracking reactive scope")
|
||||||
end
|
end
|
||||||
|
|
||||||
return scope
|
return scope
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue