Update docs

This commit is contained in:
aaron 2024-06-20 18:40:01 +01:00
parent f2de9b0e63
commit 62f1c3a20a
16 changed files with 86 additions and 347 deletions

View file

@ -49,10 +49,9 @@ export default withMermaid({
{ text: "Derived Sources", link: "/tut/crash-course/9-derived-source" },
{ text: "Cleanup", link: "/tut/crash-course/10-cleanup" },
{ text: "Control Flow", link: "/tut/crash-course/11-control-flow" },
{ text: "Property Nesting", link: "/tut/crash-course/12-property-nesting" },
{ text: "Actions", link: "/tut/crash-course/13-actions" },
{ text: "Strict Mode", link: "/tut/crash-course/14-strict-mode" },
{ text: "Concepts Summary", link: "/tut/crash-course/15-concepts" }
{ text: "Actions", link: "/tut/crash-course/12-actions" },
{ text: "Strict Mode", link: "/tut/crash-course/13-strict-mode" },
{ text: "Concepts Summary", link: "/tut/crash-course/14-concepts" }
]
},
{

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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