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
|
|
@ -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