This commit is contained in:
Alice 2024-12-29 21:38:00 +01:00
commit 63077355eb
66 changed files with 1741 additions and 1671 deletions

View file

@ -6,10 +6,16 @@ Vide is heavily inspired by [Solid](https://www.solidjs.com/).
## Why Vide?
Vide provides a reactive and declarative API to simplify managing UI.
Vide's reactive and declarative API aims to let you program UI as simply as
possible, with a strong focus on how data flows through your application.
Some of the main focuses behind Vide's design choices:
Some of Vide's main design choices:
- Minimal syntax
- Complete typechecking
- Independence from instances
- Syntax minimal.
- Data oriented.
- Typechecking compatible.
- Instance independent.
Vide's reactivity operates with the concept
of scopes which carries a learning curve, though is what makes Vide's minimal
syntax possible. The crash course will introduce these concepts gradually.

View file

@ -9,10 +9,10 @@ destroyed, or when a stable scope is destroyed.
local root = vide.root
local source = vide.source
local effect = vide.effect
local cleanup = vide.cleanup
local count = source(0)
local destroy = root(function()
effect(function()
local x = count()

View file

@ -1,98 +0,0 @@
# Control Flow
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.
The new sources can be used in `create()` to update the children of a container
instance.
## indexes()
`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.
```luau
local list = source {
"finish the crash course",
"star Vide's GitHub"
}
local function TodoList(props: { list: () -> Array<string> })
return create "Frame" {
create "UIListLayout" {},
indexes(list, function(todo, i)
return create "TextLabel" {
Text = function()
return i .. ": " .. todo()
end,
LayoutOrder = i
}
end)
}
end
TodoList { list = list }
```
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 at the index
2. the index itself
When the value at an index is changed, the function is not reran. Instead, the
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 stable scope destroyed.
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
todoList --> indexes -.- subroot1 & subroot2
subgraph subroot1 ["indexes scope 1"]
direction LR
value1[todo] --> prop1["prop binding"]
end
subgraph subroot2 ["indexes scope 2"]
direction LR
value2[todo] --> prop2[prop binding]
end
end
```
When you edit a table in a source, you must set that table again to actually
update the source.
```luau
local src = source { 1, 2 }
local data = src()
table.insert(data, 3) -- no effects will run
src(data) -- effects will run
```

View file

@ -0,0 +1,158 @@
# Dynamic Scopes
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 *dynamic scope* functions.
These functions create and destroy components for you in response to source
updates. They return a source containing the created component. This source can
be parented as a child which will update the shown children whenever the source
updates.
The simplest example is using `show()`.
```luau
local source = vide.source
local create = vide.create
local show = vide.show
local root = vide.root
function Button(props: { Text: string, Activated: () -> () })
return create "TextButton" {
Text = props.Text,
Activated = props.Activated
}
end
function Menu()
return create "TextLabel" {
Text = "This is a menu"
}
end
function App()
local toggled = source(false)
return create "ScreenGui" {
Button {
Text = "Toggle Menu",
Activated = function()
toggled(not toggled())
end
},
show(toggled, Menu) -- [!code highlight]
}
end
root(function()
App().Parent = game.StarterGui
end)
```
This is a complete example of rendering UI which has a single button that
toggles the opening of a menu.
--------------------------------------------------------------------------------
Another common function is `indexes()`. This function creates a component for
each index in a table.
Each component created is done so in a new and independent stable scope. The
indexes of the table are checked each source update to prevent redunant
destruction and recreation of UI elements.
```luau
local source = vide.source
local create = vide.create
local indexes = vide.indexes
local root = vide.root
local function Todo(props: {
Text: () -> string,
Position: number,
Activated: () -> ()
})
return create "TextButton" {
Text = function() return props.Position .. ": " .. props.Text() end,
LayoutOrder = props.Position,
Activated = Activated
}
end
local function TodoList(props: { List: () -> Array<string> })
return create "Frame" {
create "UIListLayout" {},
indexes(props.List, function(text, i) -- [!code highlight]
return Todo {
Text = text,
Position = i,
Activated = function() -- remove the todo when clicked
local list = props.List()
table.remove(list, i)
props.List(list)
end
}
end)
}
end
function App()
local list = source {
"finish the crash course",
"star Vide's GitHub"
}
return create "ScreenGui" {
TodoList { List = list },
}
end
root(function()
App().Parent = game.StarterGui
end)
```
The reactive graph for the above example:
```mermaid
%%{init: {
"theme": "base",
"themeVariables": {
"primaryColor": "#111720",
"primaryTextColor": "#fff",
"primaryBorderColor": "#111720",
"lineColor": "#79B8FF",
"tertiaryColor": "#0d131b",
"tertiaryBorderColor": "#0d131b"
}
}}%%
graph
subgraph root ["root"]
direction LR
todoList --> indexes -.- subroot1 & subroot2
subgraph subroot1 ["indexes scope 1"]
direction LR
value1[todo] --> prop1["prop binding"]
end
subgraph subroot2 ["indexes scope 2"]
direction LR
value2[todo] --> prop2[prop binding]
end
end
```
When you edit a table in a source, you must set that table again to actually
update the source.
```luau
local src = source { 1, 2 }
local data = src()
table.insert(data, 3) -- no effects will run
src(data) -- effects will run
```

View file

@ -1,13 +1,15 @@
# Actions
Actions in Vide are special callbacks that you can pass along with properties,
Actions are special callbacks that you can pass along with properties,
to run some code on an instance receiving them.
```luau
local action = vide.action
```
<<<<<<< HEAD
```luau
=======
>>>>>>> 58a31a1b329e922dc86c554e8220012ab7238f1b
create "TextLabel" {
Text = "test",
@ -24,12 +26,13 @@ action used to listen for property changes:
```luau
local action = vide.action
local source = vide.source
local effect = vide.effect
local cleanup = vide.cleanup
local function changed(prop: string, callback: (new) -> ())
local function changed(property: string, callback: (new) -> ())
return action(function(instance)
local connection = instance:GetPropertyChangedSignal(prop):Connect(function()
local connection = instance:GetPropertyChangedSignal(property):Connect(function()
callback(instance[property])
end)
@ -49,7 +52,7 @@ effect(function()
print(output())
end)
instance.Text = "foo" -- "foo" will be printed from the effect
instance.Text = "foo" -- "foo" will be printed by the effect
```
The source `output` will be updated with the new property value any time it is

View file

@ -23,6 +23,7 @@ local count = source(0)
local ran = 0
effect(function()
count()
ran += 1
end)

View file

@ -30,7 +30,7 @@ Created by:
- `root()`
- `untrack()`
- `switch()`
- `show()`
- `indexes()`
Stable scopes do not track sources and never rerun.
@ -47,23 +47,14 @@ Created by:
Reactive scopes do track sources and will rerun when those sources update.
Reactive scopes cannot be created within a reactive scope, but stable scopes
can.
can be created within a reactive scope.
## Scope Owners
## Scope Cleanup
A scope created within another scope is *owned* by the other scope, with the
exception of the scope created by `root()`.
When a scope is rerun or destroyed, all scopes created within it are
automatically destroyed.
When a scope is rerun or destroyed, all scopes owned by it are automatically
destroyed.
`root()` creates a stable scope with no owner, instead it is destroyed manually.
## Cleanup
Arbitrary code to run whenever a stable or reactive scope is rerun or destroyed.
Queue a function to run using `cleanup()`.
Any functions queued by `cleanup()` are also ran.
## Reactive Graph
@ -78,7 +69,7 @@ local count = source(0)
root(function()
local text = derive(function()
return "count: " .. text()
return "count: " .. count()
end)
effect(function()
@ -93,12 +84,12 @@ end)
%%{init: {
"theme": "base",
"themeVariables": {
"primaryColor": "#1B1B1F",
"primaryColor": "#111720",
"primaryTextColor": "#fff",
"primaryBorderColor": "#1B1B1F",
"primaryBorderColor": "#111720",
"lineColor": "#79B8FF",
"tertiaryColor": "#161618",
"tertiaryBorderColor": "#1C1C1F"
"tertiaryColor": "#0d131b",
"tertiaryBorderColor": "#202530"
}
}}%%
@ -118,6 +109,5 @@ Notes:
- An update to `count` will cause `text` to rerun, which
then causes `effect` to rerun.
- When the root scope is destroyed, `text` and
`effect` will be destroyed alongside it, since they are
owned by it. `count` will be untouched and future updates
to `count` will have no effect.
`effect` will be destroyed alongside it, since they were created within it.
`count` will be untouched and future updates to `count` will have no effect.

View file

@ -1,6 +1,6 @@
# 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.
A source and effect is analogous to a signal and connection.
Effects are created using `effect()`.
@ -23,7 +23,10 @@ count(1)
Any source read inside an effect is tracked and will rerun the effect when
that source is updated.
Derived sources are also tracked, it doesn't matter how deeply nested
The effect runs its callback once immediately to initially figure out what
sources are being read.
Derived sources are also tracked, it does not matter how deeply nested
inside a function a source is.
```luau
@ -47,3 +50,22 @@ count(2)
If a source is updated with the same value it already had, it will not rerun
effects depending on it.
You can also read from a source within an effect without the effect tracking it.
```luau
local source = vide.source
local effect = vide.effect
local untrack = vide.untrack
local a = source(0)
local b = source(0)
effect(function()
print(`a: {a()} b: {untrack(b)}`)
end)
a(1) -- prints "a: 1 b: 0"
b(1) -- prints nothing
a(2) -- prints "a: 2 b: 1"
```

View file

@ -7,38 +7,39 @@ 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.
Scopes come in two flavors; stable and reactive.
Thre are two types of scopes: stable and reactive.
- 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
- A scope must be created within another scope.
- Stable scopes never rerun.
- Reactive scopes can rerun.
- A reactive scope cannot be created within another reactive scope, only within
a stable scope.
An exception to the first rule is `root()`, which creates the initial scope that
you destroy manually with a destructor function it returns.
`effect()` creates a reactive scope.
`root()` creates a stable scope.
`effect()` creates a reactive scope.
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.
destroyed, and so on.
```luau
local root = vide.root
local source = vide.source
local effect = vide.effect
local function setup()
local count = source(0)
local count = source(0)
local function setup()
effect(function()
print(count())
end)
return count
end
setup() -- will error since effect() tries to create a reactive scope outside of a stable scope
setup() -- error, effect() tried to create a reactive scope with no stable scope
<<<<<<< HEAD
local count = root(setup) -- ok since effect() was called within a stable scope
count(1) -- prints "1"
```
@ -57,12 +58,16 @@ local function setup()
end
local destroy, count = root(setup)
=======
local destroy = root(setup) -- ok since effect() was called in a stable scope
>>>>>>> 58a31a1b329e922dc86c554e8220012ab7238f1b
count(1) -- prints "1"
count(2) -- prints "2"
destroy()
count(2) -- effect is destroyed; no longer prints
count(3) -- reactive scope created by effect() is destroyed, it does not rerun
```
Vide's reactivity can be represented graphically, as a *reactive graph*.
@ -73,12 +78,12 @@ The reactive graph for the above example looks like so:
%%{init: {
"theme": "base",
"themeVariables": {
"primaryColor": "#1B1B1F",
"primaryColor": "#111720",
"primaryTextColor": "#fff",
"primaryBorderColor": "#1B1B1F",
"primaryBorderColor": "#111720",
"lineColor": "#79B8FF",
"tertiaryColor": "#161618",
"tertiaryBorderColor": "#161618"
"tertiaryColor": "#0d131b",
"tertiaryBorderColor": "#0d131b"
}
}}%%
@ -90,7 +95,7 @@ subgraph root
end
```
When the stable `root()` is destroyed, the reactive `effect()`
When the stable `root()` scope 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

View file

@ -1,10 +1,13 @@
# Stateful Components
# Reactive Components
Stateful components in Vide are created using sources and effects - sources to
Reactive components in Vide are created using sources and effects - sources to
store the data, and effects to display the data.
<<<<<<< HEAD:docs/tut/crash-course/7-stateful-component.md
## Internal State
=======
>>>>>>> 58a31a1b329e922dc86c554e8220012ab7238f1b:docs/tut/crash-course/7-reactive-component.md
```luau
local create = vide.create
local source = vide.source
@ -33,19 +36,17 @@ 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 component.
## External State
External sources can also be passed into components for them to use.
```luau
<<<<<<< HEAD:docs/tut/crash-course/7-stateful-component.md
local function Counter(props: { count: () -> number })
=======
local function CountDisplay(props: { count: () -> number })
>>>>>>> 58a31a1b329e922dc86c554e8220012ab7238f1b:docs/tut/crash-course/7-reactive-component.md
local count = props.count
local instance = create "TextButton" {
Activated = function()
count(count() + 1)
end
}
local instance = create "TextLabel" {}
effect(function()
instance.Text = "count: " .. count()
@ -56,11 +57,11 @@ end
local count = source(0)
Counter {
CountDisplay {
count = count
}
count(1) -- the Counter component will update to display this count
count(1) -- the CountDisplay component will update to display this count
```
Sources can be created internally or passed in from externally, there are no

View file

@ -3,7 +3,13 @@
Explicitly creating effects to update properties is tedious. You can
*implicitly* create an effect to update properties instead.
<<<<<<< HEAD
```luau
=======
::: code-group
```luau [Implicit Effect]
>>>>>>> 58a31a1b329e922dc86c554e8220012ab7238f1b
local create = vide.create
local source = vide.source
@ -22,6 +28,30 @@ local function Counter()
end
```
```luau [Explicit Effect]
local create = vide.create
local source = vide.source
local effect = vide.effect
local function Counter()
local count = source(0)
local instance = create "TextButton" {
Activated = function()
count(count() + 1)
end
}
effect(function()
instance.Text = "count: " .. count()
end)
return instance
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
@ -46,12 +76,12 @@ local function List(props: { children: () -> { Instance } })
}
end
local list = List { children = items } -- creates a list with a single text label "A"
local list = List { children = items } -- creates a list with text label "A"
items {
create "TextLabel" { Text = "B" },
create "TextLabel" { Text = "C" }
}
-- this will automatically unparent the text label "A", and parent the labels "B" and "C"
-- this will automatically unparent text label "A", and parent labels "B" and "C"
```

View file

@ -57,7 +57,8 @@ effect(function() text() end)
count(1) -- prints "ran" x1
```
`derive()` must also be called within a stable scope, just like `effect()`.
Because `derive()` creates a reactive scope, it must be called within a stable
scope, just like `effect()`.
If the recalculated value is the same as the old value, the derived source will
not rerun the effects using it.
@ -68,12 +69,12 @@ The reactive graph for the above example:
%%{init: {
"theme": "base",
"themeVariables": {
"primaryColor": "#1B1B1F",
"primaryColor": "#111720",
"primaryTextColor": "#fff",
"primaryBorderColor": "#1B1B1F",
"primaryBorderColor": "#111720",
"lineColor": "#79B8FF",
"tertiaryColor": "#161618",
"tertiaryBorderColor": "#161618"
"tertiaryColor": "#0d131b",
"tertiaryBorderColor": "#0d131b"
}
}}%%
@ -86,7 +87,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.
another source. You should avoid doing 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.