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
f2de9b0e63
commit
62f1c3a20a
16 changed files with 86 additions and 347 deletions
|
|
@ -4,22 +4,12 @@ This is a tutorial that introduces the concepts and usage of Vide.
|
|||
|
||||
Vide is heavily inspired by [Solid](https://www.solidjs.com/).
|
||||
|
||||
This tutorial assumes familiarity with Luau and Roblox UI.
|
||||
|
||||
## Why Vide?
|
||||
|
||||
Creating UI is complicated, slow, and tedious.
|
||||
|
||||
Vide tries to simplify and speed up this process by providing a declarative and
|
||||
reactive of style programming, which lets you focus more on designing the UI
|
||||
itself and not having to manually update or reparent UI instances.
|
||||
Vide provides a reactive and declarative API to simplify managing UI.
|
||||
|
||||
Some of the main focuses behind Vide's design choices:
|
||||
|
||||
- Minimal syntax.
|
||||
- Minimal syntax
|
||||
- Complete typechecking
|
||||
- Independence from instances.
|
||||
|
||||
As with most declarative libraries, there is an initial learning curve to
|
||||
understand the concepts and usage. This tutorial tries to comprehensively
|
||||
cover these concepts and usage, more so than you need just to use it.
|
||||
- Independence from instances
|
||||
|
|
|
|||
|
|
@ -2,48 +2,33 @@
|
|||
|
||||
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 is rerun
|
||||
or destroyed, or when a stable scope is destroyed.
|
||||
is used to queue a callback for the next time a reactive scope is rerun or
|
||||
destroyed, or when a stable scope is destroyed.
|
||||
|
||||
```lua
|
||||
local mount = vide.mount
|
||||
local root = vide.root
|
||||
local source = vide.source
|
||||
local cleanup = vide.cleanup
|
||||
local effect = vide.effect
|
||||
|
||||
local function Timer()
|
||||
local count = source(0)
|
||||
local count = source(0)
|
||||
|
||||
local con = game:GetService("RunService").Heartbeat:Connect(function(dt)
|
||||
count(count() + dt)
|
||||
|
||||
local destroy = root(function(destroy)
|
||||
effect(function()
|
||||
local x = count()
|
||||
cleanup(function() print(x) end)
|
||||
end)
|
||||
|
||||
cleanup(function()
|
||||
con:Disconnect()
|
||||
end)
|
||||
cleanup(function() print "root destroyed" end)
|
||||
|
||||
return create "TextButton" {
|
||||
Position = UDim2.fromOffset(300, 300),
|
||||
Size = UDim2.fromOffset(200, 50),
|
||||
|
||||
Text = function()
|
||||
return "seconds: " .. math.floor(count())
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local instance, destroy = root(function(destroy)
|
||||
local instance = Timer()
|
||||
return instance, destroy
|
||||
return destroy
|
||||
end)
|
||||
|
||||
wait(5)
|
||||
|
||||
destroy() -- all queued cleanups are ran, heartbeat connection disconnected
|
||||
count(1) -- prints "0"
|
||||
count(2) -- prints "1"
|
||||
destroy() -- prints "2" and "root destroyed"
|
||||
```
|
||||
|
||||
In the above example, this allows us to disconnect the heartbeat connection
|
||||
when the scope responsible for creating the timer component is destroyed.
|
||||
|
||||
::: 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
|
||||
|
|
|
|||
|
|
@ -5,119 +5,26 @@ 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.
|
||||
|
||||
Control flow functions run their components in a new stable scope, which can
|
||||
be destroyed independently of the stable scope that called the control flow
|
||||
function. This means parts of your app can be independently created and
|
||||
destroyed.
|
||||
|
||||
## switch()
|
||||
|
||||
`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
|
||||
|
||||
return create "Frame" {
|
||||
switch(joined) {
|
||||
[true] = LeaveButton,
|
||||
[false] = 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["root scope"]
|
||||
direction LR
|
||||
joined --> switch -.- subroot
|
||||
|
||||
subgraph subroot["switch scope"]
|
||||
direction LR
|
||||
effect["TextColor3 effect"]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
A `switch()` call creates a new effect and a new stable 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.
|
||||
The new sources can be used in `create()` to update the children of a container
|
||||
instance.
|
||||
|
||||
## 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 each corresponding to a
|
||||
table index, to display the value at that index.
|
||||
`indexes()` *maps* each table index to a new UI element that can
|
||||
update to display the current value at that index. Each table index is given a
|
||||
single corresponding UI element.
|
||||
|
||||
```lua
|
||||
local todoList = source {
|
||||
local list = source {
|
||||
"finish the crash course",
|
||||
"star vide's GitHub"
|
||||
"star Vide's GitHub"
|
||||
}
|
||||
|
||||
local function TodoList(props: { list: () -> Array<string> })
|
||||
return create "Frame" {
|
||||
create "UIListLayout" {},
|
||||
|
||||
indexes(todoList, function(todo, i)
|
||||
indexes(list, function(todo, i)
|
||||
return create "TextLabel" {
|
||||
Text = function()
|
||||
return i .. ": " .. todo()
|
||||
|
|
@ -129,13 +36,13 @@ local function TodoList(props: { list: () -> Array<string> })
|
|||
}
|
||||
end
|
||||
|
||||
TodoList { list = todoList }
|
||||
TodoList { list = list }
|
||||
```
|
||||
|
||||
For each index in the given source table, the given function will be called
|
||||
with:
|
||||
For each index in the given source table, the given function to `indexes()` will
|
||||
be run in a new stable scope with:
|
||||
|
||||
1. a source containing the value of the index
|
||||
1. a source containing the value at the index
|
||||
2. the index itself
|
||||
|
||||
When the value at an index is changed, the function is not reran. Instead, the
|
||||
|
|
@ -143,12 +50,9 @@ given source for that index is updated.
|
|||
|
||||
Any time the input source table is updated, the given function will be ran for
|
||||
any newly added indexes, while any removed indexes (indexes now with a `nil`
|
||||
value), will have its corresponding reactive scope destroyed to clean up that
|
||||
element.
|
||||
value), will have its corresponding stable scope destroyed.
|
||||
|
||||
|
||||
`indexes()` is said to *map* each table index to a new UI element that can
|
||||
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:
|
||||
|
||||
|
|
@ -183,8 +87,8 @@ subgraph root ["root scope"]
|
|||
end
|
||||
```
|
||||
|
||||
One thing to note regarding table sources, is that when you edit a table in a
|
||||
source, you must set that table again to actually update the source.
|
||||
When you edit a table in a source, you must set that table again to actually
|
||||
update the source.
|
||||
|
||||
```lua
|
||||
local src = source { 1, 2 }
|
||||
|
|
@ -192,11 +96,3 @@ local data = src()
|
|||
table.insert(data, 3) -- no effects will run
|
||||
src(data) -- effects will run
|
||||
```
|
||||
|
||||
Together, these control flow functions cover the majority of cases where you
|
||||
need to dynamically create and destroy parts of your UI.
|
||||
|
||||
If you need to do something that these control flow functions cannot, you can
|
||||
always use `mount()` within an effect to dynamically create and destroy
|
||||
components on your own terms. Just remember to use `cleanup()` to unmount when
|
||||
the effect reruns.
|
||||
|
|
|
|||
|
|
@ -1,120 +0,0 @@
|
|||
# Nested Properties
|
||||
|
||||
Often when creating components from existing components, you can find yourself
|
||||
repetitively passing through properties such as size or position.
|
||||
|
||||
Example below:
|
||||
|
||||
```lua
|
||||
function Background(props: {
|
||||
Color: Color3,
|
||||
AnchorPoint: UDim2,
|
||||
Position: UDim2,
|
||||
Size: UDim2
|
||||
})
|
||||
return create "Frame" {
|
||||
Color = props.Color
|
||||
AnchorPoint = props.AnchorPoint,
|
||||
Position = props.Position,
|
||||
Size = props.Size
|
||||
}
|
||||
end
|
||||
|
||||
function Menu(props: {
|
||||
Color = props.Color
|
||||
AnchorPoint: UDim2,
|
||||
Position: UDim2,
|
||||
Size: UDim2
|
||||
})
|
||||
return Background {
|
||||
Color = props.Color,
|
||||
AnchorPoint = props.AnchorPoint,
|
||||
Position = props.Position,
|
||||
Size = props.Size
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
One way this can be avoided is by using *property nesting*. In Vide, passing a
|
||||
table value inside `props` has special semantics. Any key with a table value is
|
||||
not assigned like a property, instead the table is iterated and processed just
|
||||
like the outer table is. Any properties in the nested table will be assigned
|
||||
to the instance just the same.
|
||||
|
||||
Below is an example of how you can use this to pass groups of similar properties
|
||||
together such as position and size, while also using typechecking.
|
||||
|
||||
```lua
|
||||
type Layout = {
|
||||
Layout = {
|
||||
Position: UDim2?,
|
||||
Size: UDim2?,
|
||||
AnchorPoint: Vector2?
|
||||
}
|
||||
}
|
||||
|
||||
function Background(props: Layout & { Color: Color3 })
|
||||
return create "Frame" {
|
||||
Color = props.Color,
|
||||
props.Layout
|
||||
}
|
||||
end
|
||||
|
||||
function Menu(props: Layout & { Color: Color3 })
|
||||
return Background {
|
||||
Color = props.Color,
|
||||
Layout = props.Layout
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
Here we created a nested group with the key `Layout` that can accept
|
||||
layout-related properties. Any name could be chosen for the key.
|
||||
This allows us to write much more concise syntax that is also typecheckable.
|
||||
|
||||
In another example we use a key named `Children` to pass arrays of instances to
|
||||
be parented.
|
||||
|
||||
```lua
|
||||
type Children = {
|
||||
-- also can optionally pass a source that returns an array of children too
|
||||
Children = Array<Instance> | () -> Array<Instance>
|
||||
}
|
||||
|
||||
local function List(props: Children & Layout)
|
||||
return create "Frame" {
|
||||
props.Children,
|
||||
props.Layout,
|
||||
create "UIListLayout" {}
|
||||
}
|
||||
end
|
||||
|
||||
List {
|
||||
Layout = {
|
||||
Position = UDim2.new()
|
||||
},
|
||||
|
||||
Children = {
|
||||
create "TextLabel" { Text = "1" },
|
||||
create "TextLabel" { Text = "2" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Deeper nested properties are guaranteed to be set after shallower nested
|
||||
properties, this can be used to create overridable default properties.
|
||||
|
||||
```lua
|
||||
local function List(props: Children & Layout)
|
||||
return create "Frame" {
|
||||
props.Children,
|
||||
props.Layout,
|
||||
|
||||
-- can be overriden by `props.Layout`
|
||||
AnchorPoint = Vector2.new(0.5, 0),
|
||||
Position = UDim2.fromScale(0.5, 0),
|
||||
|
||||
create "UIListLayout" {}
|
||||
}
|
||||
end
|
||||
```
|
||||
|
|
@ -2,11 +2,8 @@
|
|||
|
||||
Instances are created using `create()`.
|
||||
|
||||
`create()` returns a constructor for a class which then takes a table of
|
||||
properties to assign when creating a new instance for that class.
|
||||
|
||||
Luau allows us to omit parentheses `()` when calling functions with string or
|
||||
table literals which is recommended to use for brevity.
|
||||
Parentheses `()` can be omitted when calling functions with string or
|
||||
table literals which is recommended for brevity.
|
||||
|
||||
```lua
|
||||
local create = vide.create
|
||||
|
|
|
|||
|
|
@ -34,12 +34,12 @@ end
|
|||
return Button
|
||||
```
|
||||
|
||||
```lua [App.luau]
|
||||
```lua [Menu.luau]
|
||||
local create = vide.create
|
||||
|
||||
local Button = require(Button)
|
||||
|
||||
local function App()
|
||||
local function Menu()
|
||||
return create "ScreenGui" {
|
||||
Button {
|
||||
Position = UDim2.fromOffset(200, 200),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Sources
|
||||
|
||||
Sources are special objects that store a single value. They are the core of
|
||||
Vide's reactivity. They are called sources because they act as sources of data.
|
||||
Sources are special objects that store a single value and are the core of
|
||||
Vide's reactivity.
|
||||
|
||||
A source can be created using `source()`.
|
||||
|
||||
|
|
@ -20,8 +20,7 @@ by calling it with no arguments.
|
|||
count(count() + 1) -- increment count by 1
|
||||
```
|
||||
|
||||
Sources can be *derived* by wrapping them in functions. A wrapped source
|
||||
effectively becomes a new source.
|
||||
Sources can be *derived* by wrapping them in functions.
|
||||
|
||||
```lua
|
||||
local count = source(0)
|
||||
|
|
@ -35,7 +34,5 @@ count(1)
|
|||
print(text()) -- "count: 1"
|
||||
```
|
||||
|
||||
Sources on their own aren't very special, the above can be achieved with plain
|
||||
variables. The real use for sources become apparent when used in combination
|
||||
with *effects*. Similar to a signal and connection, a source and effect allows
|
||||
you to do things like automatically updating UI when a source is updated.
|
||||
While the above can be achieved with plain variables, the use for sources will
|
||||
be obvious in the next part.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
# Effects
|
||||
|
||||
Effects are functions that are ran in response to source updates. They are
|
||||
called effects because they cause *side-effects* when reacting to source
|
||||
updates.
|
||||
A source and effect is analogous to a signal and connection.
|
||||
|
||||
Effects are created using `effect()`.
|
||||
|
||||
|
|
@ -21,11 +20,10 @@ count(1)
|
|||
-- "count: 1" printed
|
||||
```
|
||||
|
||||
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.
|
||||
Any source read inside an effect is tracked and will rerun the effect when
|
||||
that source is updated.
|
||||
|
||||
Reactive scopes also track derived sources, it doesn't matter how deeply nested
|
||||
Derived sources are also tracked, it doesn't matter how deeply nested
|
||||
inside a function a source is.
|
||||
|
||||
```lua
|
||||
|
|
|
|||
|
|
@ -1,25 +1,29 @@
|
|||
# Scopes
|
||||
|
||||
Vide operates on the concept of scopes. Vide scopes come in two flavors:
|
||||
stable and reactive.
|
||||
Just like how a signal's connection may need to be disconnected, a source's
|
||||
effect also may need to be disconnected.
|
||||
|
||||
The three main rules for scopes are:
|
||||
But the disconnecting of many signals and connections is tedious and verbose.
|
||||
Vide instead operates on the concept of scopes which provides a much cleaner
|
||||
API, given that you follow a few rules.
|
||||
|
||||
- Stable scopes never rerun.
|
||||
- Reactive scopes will rerun on source updates.
|
||||
- A reactive scope cannot be created within another reactive scope.
|
||||
Scopes come in two flavors; stable and reactive.
|
||||
|
||||
Reactive scopes cannot be created on their own - they must be created within
|
||||
a stable scope so that it can be tracked and later destroyed when it is
|
||||
no longer needed.
|
||||
- All scopes must be created within another scope with the exception of `root()`
|
||||
- Stable scopes never rerun
|
||||
- Reactive scopes can rerun
|
||||
- A reactive scope cannot be created within another reactive scope
|
||||
|
||||
This is the purpose of `root()`, which creates an initial stable scope, which
|
||||
all other reactive scopes, such as ones created by `effect()`, can stem from.
|
||||
`effect()` creates a reactive scope.
|
||||
`root()` creates a stable scope.
|
||||
|
||||
When this root reactive scope is destroyed, it will destroy any effects created
|
||||
within it, ensuring everything is cleaned up properly.
|
||||
Whenever a scope is destroyed, any scope created within that scope is also
|
||||
destroyed, and so on. This is why all scopes must be created within another
|
||||
scope, except `root()` which is used to create the initial scope that you can
|
||||
manually destroy.
|
||||
|
||||
```lua
|
||||
local root = vide.root
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
|
||||
|
|
@ -33,9 +37,9 @@ local function setup()
|
|||
return count
|
||||
end
|
||||
|
||||
setup() -- will error since effect() was not called within a stable scope
|
||||
setup() -- will error since effect() tries to create a reactive scope outside of a stable scope
|
||||
|
||||
local count = vide.root(setup) -- runs
|
||||
local count = root(setup) -- ok since effect() was called within a stable scope
|
||||
count(1) -- prints "1"
|
||||
```
|
||||
|
||||
|
|
@ -87,7 +91,7 @@ subgraph root
|
|||
end
|
||||
```
|
||||
|
||||
When the root reactive scope created by `root()` is destroyed, the `effect`
|
||||
When the stable `root()` is destroyed, the reactive `effect()`
|
||||
scope will also be destroyed since it was created within it.
|
||||
|
||||
This is important because you may have an effect that updates the property of a
|
||||
|
|
@ -96,7 +100,6 @@ 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 reactive scope, since you should be creating all your UI and corresponding
|
||||
effects within a top-level `root()` 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.
|
||||
stable scope, since you should be creating all your UI and effects within a
|
||||
single top-level `root()` call that puts all your UI together, making it safe to
|
||||
assume any effect created will be created under this stable scope.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
# Stateful Components
|
||||
|
||||
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.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Implicit Effects
|
||||
|
||||
Explicitly creating effects to update properties can be tedious. Vide provides a
|
||||
way to *implicitly* create an effect to update properties.
|
||||
Explicitly creating effects to update properties is tedious. You can
|
||||
*implicitly* create an effect to update properties instead.
|
||||
|
||||
```lua
|
||||
local create = vide.create
|
||||
|
|
@ -25,18 +25,14 @@ end
|
|||
This example is equivalent to the example seen on the previous page.
|
||||
|
||||
Instead of explicitly creating an effect, assigning a (non-event) property a
|
||||
function will implicitly create an effect to update that property anytime a
|
||||
source used within is updated.
|
||||
|
||||
Just like effects, the function is ran immediately in a reactive scope to set
|
||||
the property initially and determine what sources are being used.
|
||||
function will implicitly create an effect to update that property.
|
||||
|
||||
## Children
|
||||
|
||||
Children can also be set in a similar manner. A source passed as a child (passed
|
||||
with a number key instead of string key) can return an instance or an array of
|
||||
instances. Vide will automatically unparent removed instances and parent new
|
||||
instances when that source's stored instances change.
|
||||
instances. An effect is automatically created to unparent removed instances and
|
||||
parent new instances on source update.
|
||||
|
||||
```lua
|
||||
local items = source {
|
||||
|
|
@ -57,5 +53,5 @@ items {
|
|||
create "TextLabel" { Text = "C" }
|
||||
}
|
||||
|
||||
-- this will automatically unparent the text label "A", and parent the labels "B" and "C".
|
||||
-- this will automatically unparent the text label "A", and parent the labels "B" and "C"
|
||||
```
|
||||
|
|
|
|||
|
|
@ -36,14 +36,13 @@ source(1) -- prints "ran" x2
|
|||
```
|
||||
|
||||
To avoid this, you can use `derive()` to derive a new source instead. This will
|
||||
run a function in a new reactive scope only when a dependent source has updated.
|
||||
Reading this derived source multiple times will just return a cached result from
|
||||
when it last updated.
|
||||
run a function in a reactive scope only when a source used inside updated.
|
||||
Reading this derived source multiple times will just return a cached result.
|
||||
|
||||
```lua
|
||||
local source = vide.source
|
||||
local derive = vide.derive
|
||||
local effect = vide.effect
|
||||
local derive = vide.derive
|
||||
|
||||
local count = source(0)
|
||||
|
||||
|
|
@ -87,6 +86,7 @@ end
|
|||
```
|
||||
|
||||
Deriving a source in this manner is similar to creating an effect to update
|
||||
another source. You should never manually do this using an effect however,
|
||||
improper usage could accidently create infinite loops in the reactive graph.
|
||||
Always favour deriving when you need one source to update based on another.
|
||||
another source. You should never manually do this using an effect however.
|
||||
Improper usage could accidently create infinite loops in the reactive graph.
|
||||
Always favour deriving when you need one source to update based on another
|
||||
source.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue