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: "Derived Sources", link: "/tut/crash-course/9-derived-source" },
{ text: "Cleanup", link: "/tut/crash-course/10-cleanup" }, { text: "Cleanup", link: "/tut/crash-course/10-cleanup" },
{ text: "Control Flow", link: "/tut/crash-course/11-control-flow" }, { 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/12-actions" },
{ text: "Actions", link: "/tut/crash-course/13-actions" }, { text: "Strict Mode", link: "/tut/crash-course/13-strict-mode" },
{ text: "Strict Mode", link: "/tut/crash-course/14-strict-mode" }, { text: "Concepts Summary", link: "/tut/crash-course/14-concepts" }
{ text: "Concepts Summary", link: "/tut/crash-course/15-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/). Vide is heavily inspired by [Solid](https://www.solidjs.com/).
This tutorial assumes familiarity with Luau and Roblox UI.
## Why Vide? ## Why Vide?
Creating UI is complicated, slow, and tedious. Vide provides a reactive and declarative API to simplify managing UI.
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.
Some of the main focuses behind Vide's design choices: Some of the main focuses behind Vide's design choices:
- Minimal syntax. - Minimal syntax
- Complete typechecking - Complete typechecking
- Independence from instances. - 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.

View file

@ -2,48 +2,33 @@
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 is rerun is used to queue a callback for the next time a reactive scope is rerun or
or destroyed, or when a stable scope is destroyed. destroyed, or when a stable scope is destroyed.
```lua ```lua
local mount = vide.mount local root = vide.root
local source = vide.source 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) end)
cleanup(function() cleanup(function() print "root destroyed" end)
con:Disconnect()
return destroy
end) end)
return create "TextButton" { count(1) -- prints "0"
Position = UDim2.fromOffset(300, 300), count(2) -- prints "1"
Size = UDim2.fromOffset(200, 50), destroy() -- prints "2" and "root destroyed"
Text = function()
return "seconds: " .. math.floor(count())
end,
}
end
local instance, destroy = root(function(destroy)
local instance = Timer()
return instance, destroy
end)
wait(5)
destroy() -- all queued cleanups are ran, heartbeat connection disconnected
``` ```
In the above example, this allows us to disconnect the heartbeat connection
when the scope responsible for creating the timer component is destroyed.
::: tip ::: tip
Roblox instances do not need to be explicitly destroyed for their 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

View file

@ -5,119 +5,26 @@ 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.
The new sources can be used in `create()` to update the children of a container
Control flow functions run their components in a new stable scope, which can instance.
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.
## indexes() ## indexes()
Often, you will have a table of values with each value displayed in a similar `indexes()` *maps* each table index to a new UI element that can
manner. Rather than manually looping over each value to generate a corresponding update to display the current value at that index. Each table index is given a
UI element, `indexes()` allows you to create elements each corresponding to a single corresponding UI element.
table index, to display the value at that index.
```lua ```lua
local todoList = source { local list = source {
"finish the crash course", "finish the crash course",
"star vide's GitHub" "star Vide's GitHub"
} }
local function TodoList(props: { list: () -> Array<string> }) local function TodoList(props: { list: () -> Array<string> })
return create "Frame" { return create "Frame" {
create "UIListLayout" {}, create "UIListLayout" {},
indexes(todoList, function(todo, i) indexes(list, function(todo, i)
return create "TextLabel" { return create "TextLabel" {
Text = function() Text = function()
return i .. ": " .. todo() return i .. ": " .. todo()
@ -129,13 +36,13 @@ local function TodoList(props: { list: () -> Array<string> })
} }
end end
TodoList { list = todoList } TodoList { list = list }
``` ```
For each index in the given source table, the given function will be called For each index in the given source table, the given function to `indexes()` will
with: 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 2. the index itself
When the value at an index is changed, the function is not reran. Instead, the 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 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` 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 value), will have its corresponding stable scope destroyed.
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. Each table index is given a
single corresponding UI element.
The reactive graph for the above example: The reactive graph for the above example:
@ -183,8 +87,8 @@ subgraph root ["root scope"]
end end
``` ```
One thing to note regarding table sources, is that when you edit a table in a When you edit a table in a source, you must set that table again to actually
source, you must set that table again to actually update the source. update the source.
```lua ```lua
local src = source { 1, 2 } local src = source { 1, 2 }
@ -192,11 +96,3 @@ local data = src()
table.insert(data, 3) -- no effects will run table.insert(data, 3) -- no effects will run
src(data) -- 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()`. Instances are created using `create()`.
`create()` returns a constructor for a class which then takes a table of Parentheses `()` can be omitted when calling functions with string or
properties to assign when creating a new instance for that class. table literals which is recommended for brevity.
Luau allows us to omit parentheses `()` when calling functions with string or
table literals which is recommended to use for brevity.
```lua ```lua
local create = vide.create local create = vide.create

View file

@ -34,12 +34,12 @@ end
return Button return Button
``` ```
```lua [App.luau] ```lua [Menu.luau]
local create = vide.create local create = vide.create
local Button = require(Button) local Button = require(Button)
local function App() local function Menu()
return create "ScreenGui" { return create "ScreenGui" {
Button { Button {
Position = UDim2.fromOffset(200, 200), Position = UDim2.fromOffset(200, 200),

View file

@ -1,7 +1,7 @@
# Sources # Sources
Sources are special objects that store a single value. They are the core of Sources are special objects that store a single value and are the core of
Vide's reactivity. They are called sources because they act as sources of data. Vide's reactivity.
A source can be created using `source()`. A source can be created using `source()`.
@ -20,8 +20,7 @@ by calling it with no arguments.
count(count() + 1) -- increment count by 1 count(count() + 1) -- increment count by 1
``` ```
Sources can be *derived* by wrapping them in functions. A wrapped source Sources can be *derived* by wrapping them in functions.
effectively becomes a new source.
```lua ```lua
local count = source(0) local count = source(0)
@ -35,7 +34,5 @@ count(1)
print(text()) -- "count: 1" print(text()) -- "count: 1"
``` ```
Sources on their own aren't very special, the above can be achieved with plain While the above can be achieved with plain variables, the use for sources will
variables. The real use for sources become apparent when used in combination be obvious in the next part.
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.

View file

@ -1,8 +1,7 @@
# Effects # Effects
Effects are functions that are ran in response to source updates. They are Effects are functions that are ran in response to source updates. They are
called effects because they cause *side-effects* when reacting to source A source and effect is analogous to a signal and connection.
updates.
Effects are created using `effect()`. Effects are created using `effect()`.
@ -21,11 +20,10 @@ count(1)
-- "count: 1" printed -- "count: 1" printed
``` ```
The callback given to `effect()` is ran immediately in a *reactive scope*. Any Any source read inside an effect is tracked and will rerun the effect when
source read from inside a reactive scope will be tracked, so when any of those that source is updated.
sources update, the effect will be reran too.
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. inside a function a source is.
```lua ```lua

View file

@ -1,25 +1,29 @@
# Scopes # Scopes
Vide operates on the concept of scopes. Vide scopes come in two flavors: Just like how a signal's connection may need to be disconnected, a source's
stable and reactive. 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. Scopes come in two flavors; stable and reactive.
- Reactive scopes will rerun on source updates.
- A reactive scope cannot be created within another reactive scope.
Reactive scopes cannot be created on their own - they must be created within - All scopes must be created within another scope with the exception of `root()`
a stable scope so that it can be tracked and later destroyed when it is - Stable scopes never rerun
no longer needed. - 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 `effect()` creates a reactive scope.
all other reactive scopes, such as ones created by `effect()`, can stem from. `root()` creates a stable scope.
When this root reactive scope is destroyed, it will destroy any effects created Whenever a scope is destroyed, any scope created within that scope is also
within it, ensuring everything is cleaned up properly. 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 ```lua
local root = vide.root
local source = vide.source local source = vide.source
local effect = vide.effect local effect = vide.effect
@ -33,9 +37,9 @@ local function setup()
return count return count
end 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" count(1) -- prints "1"
``` ```
@ -87,7 +91,7 @@ subgraph root
end 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. 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 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. 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 reactive scope, since you should be creating all your UI and corresponding stable scope, since you should be creating all your UI and effects within a
effects within a top-level `root()` call that puts all your UI together. So it single top-level `root()` call that puts all your UI together, making it safe to
is safe to assume that any effect you create will be created under this top assume any effect created will be created under this stable scope.
level scope. Vide will prevent you from accidently doing otherwise anyways.

View file

@ -1,7 +1,5 @@
# Stateful Components # 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 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.

View file

@ -1,7 +1,7 @@
# Implicit Effects # Implicit Effects
Explicitly creating effects to update properties can be tedious. Vide provides a Explicitly creating effects to update properties is tedious. You can
way to *implicitly* create an effect to update properties. *implicitly* create an effect to update properties instead.
```lua ```lua
local create = vide.create local create = vide.create
@ -25,18 +25,14 @@ end
This example is equivalent to the example seen on the previous page. This example is equivalent to the example seen on the previous page.
Instead of explicitly creating an effect, assigning a (non-event) property a Instead of explicitly creating an effect, assigning a (non-event) property a
function will implicitly create an effect to update that property anytime a function will implicitly create an effect to update that property.
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.
## Children ## Children
Children can also be set in a similar manner. A source passed as a child (passed 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 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. An effect is automatically created to unparent removed instances and
instances when that source's stored instances change. parent new instances on source update.
```lua ```lua
local items = source { local items = source {
@ -57,5 +53,5 @@ items {
create "TextLabel" { Text = "C" } 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 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. 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 from Reading this derived source multiple times will just return a cached result.
when it last updated.
```lua ```lua
local source = vide.source local source = vide.source
local derive = vide.derive
local effect = vide.effect local effect = vide.effect
local derive = vide.derive
local count = source(0) local count = source(0)
@ -87,6 +86,7 @@ end
``` ```
Deriving a source in this manner is similar to creating an effect to update 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, another source. You should never manually do this using an effect however.
improper usage could accidently create infinite loops in the reactive graph. Improper usage could accidently create infinite loops in the reactive graph.
Always favour deriving when you need one source to update based on another. Always favour deriving when you need one source to update based on another
source.