Merge reactive scope refactor

This commit is contained in:
aaron 2023-09-15 12:54:42 +01:00
parent 0e439f084f
commit efc4798ddb
48 changed files with 2750 additions and 1949 deletions

View file

@ -3,7 +3,7 @@
This is a brief tutorial designed to give you a quick run through the usage of
Vide.
Vide is largely inspired by other UI libraries such as Solid and Fusion.
Vide is heavily inspired by [Solid](https://www.solidjs.com/).
## Why Vide?
@ -19,7 +19,29 @@ Some of the main focuses behind Vide's design choices:
- Reducing the amount of imports needed for usage by leveraging Luau's syntax
and semantics.
- Being completely typecheckable.
- Flexibility, particularly with integrating other libraries and allowing users
to use their own patterns.
- A powerful reactive system that does not interfere with the lifetime of
instances.
- Flexibility with integrating other libraries and allowing users to use their
own patterns.
- Independence from instance lifetimes.
- A powerful reactive system that can update specific properties as a result of
state changes, updates are immediate with no diffing needed.
## Structure Of A Vide App
The entry point for all Vide apps is the `mount()` function. This function
sets up Vide's reactivity system. It takes and calls a function that should
create your entire app, and will apply its result to a target.
In Vide, your app should be composed of functions, each function creates a
specific part of your app, and can be reused if needed. These functions are
called *components*.
```lua
local function App()
return create "ScreenGui" {
create "TextLabel" { Text = "hi" }
}
end
mount(App, game.StarterGui)
```

View file

@ -1,62 +1,54 @@
# Creating UI Elements
# Creating UI
Instances are created using [`create()`](../../api/creation.md#create).
```lua
local vide = require(path_to_vide)
local create = vide.create
```
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 for brevity.
table literals which Vide takes advantage of for brevity.
```lua
local frame = create "Frame" {
Name = "Background",
Position = UDim2.fromScale(0.5, 0.5)
}
```
local vide = require(vide)
local mount = vide.mount
local create = vide.create
String keys are treated as properties and integer keys are treated as child
instances.
local function App()
return create "ScreenGui" {
create "Frame" {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromScale(0.4, 0.7),
```lua
create "ScreenGui" {
Parent = game.StarterGui,
create "TextLabel" {
Text = "hi"
},
create "Frame" {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromScale(0.4, 0.7),
create "TextLabel" {
Text = "bye"
},
create "TextLabel" {
Text = "hi"
},
create "TextButton" {
Text = "click me",
create"TextLabel" {
Text = "bye"
Activated = function()
print "clicked!"
end
}
}
}
}
end
mount(App, game.StarterGui)
```
To connect to an event, just assign the event property a function.
Assign a value to a string key to set a property, and assign a value to a
number key to set a child. Events can be connected to by assigning a function
to a string key.
All event arguments are passed into the function.
```lua
create "TextButton" {
Activated = function()
print "clicked!"
end
}
```
You can also use a form of aggregate initialization to create datatypes instead
of explicitly typing out the class name and constructor.
You can also use a shorthand to create datatypes instead of explicitly typing
out the class name and constructor. The table will be unpacked into the `.new()`
constructor of the property's type.
```lua
create "Frame" {
@ -64,7 +56,3 @@ create "Frame" {
UDim2 = { 0.5, 0, 0.5, 0 }
}
```
When a property is assigned a table, Vide will inspect the type of the property
being assigned to, and call that type's default `new()` constructor with the
unpacked values from the assigned table.

View file

@ -5,7 +5,10 @@ Components are custom-made reusable pieces of UI made from other pieces of UI.
By using components you can make your application more modular and better
organized.
```lua
```lua [Button.luau]
local vide = require(vide)
local create = vide.create
local function Button(props: {
Position: UDim2,
Text: string,
@ -20,29 +23,44 @@ local function Button(props: {
Activated = props.Activated
}
end
return Button
```
Above is a simple example of a button component with its background color set to
a dark grey and with a fixed size.
```lua [App.luau]
local vide = require(vide)
local mount = vide.mount
local create = vide.create
local Button = require(Button)
local function App()
return create "ScreenGui" {
Button {
Position = UDim2.fromOffset(200, 200),
Text = "click me!",
Activated = function()
print "clicked"
end
}
}
end
mount(App, game.StarterGui)
```
Above is a simple example of a button component with a set color and size,
being reused across files.
A single parameter `props` is used to pass properties to the component.
Creating instances of this button component is as simple as doing the below:
```lua
local button = Button {
Position = UDim2.new(),
Text = "Click me!",
Activated = function()
print "clicked"
end
}
```
Components allow you to *encapsulate* behavior. You can only modify the
component in ways that you allow in the component.
This also promotes code reusability. Anytime you want a new button all you do
is call `Button {}` instead of creating and setting every property each time.
When changing the button in future, any changes to the button file will be
reflected anywhere the button is used throughout your app.
This can be extended to much more complicated UI.

View file

@ -4,33 +4,18 @@
core of reactivity in Vide, as updates to a source can automatically update
properties or other sources depending on that source.
A source in Vide can be created using
[`source()`](../../api/reactivity-core.md#source).
A source in Vide can be created using `source()`.
```lua
local vide = require(vide)
local source = vide.source
local count = source(0)
```
The value passed to `source()` is the initial value of the source.
The value of a source can be set by calling it with an argument, and can be read
by calling it with no arguments.
```lua
count(count() + 1) -- increment source by 1
```
Below is an example of a stateful counter component.
```lua
local function Counter(props: { Position: UDim2 })
local function Counter()
local count = source(0)
return create "TextButton" {
Position = props.Position,
Size = UDim2.new(200, 50),
Position = UDim2.fromOffset(300, 300),
Size = UDim2.fromOffset(200, 50),
Text = count,
@ -39,37 +24,28 @@ local function Counter(props: { Position: UDim2 })
end
}
end
mount(function() return create "ScreenGui" { Counter {} } end, game.StarterGui)
```
Each call of `Counter {}` will create a new counter element, each with their own
independent count.
The value passed to `source()` is the initial value of the source.
Vide detects when you assign a function to a property. This is known
as *binding* and doing so will cause the property to *automatically* update
whenever a source in that function is updated, by rerunning the function and
assigning its return value. You can only bind non-event
properties, otherwise the function is connected as the event callback.
This allows you as the programmer to not need to manually update GUI as the state
of your program changes. You just define how the data maps to UI, and Vide's
reactive system will surgically update any properties depending on sources that
are changed.
Since sources are just functions, you can also pass external sources to
components like so:
The value of a source can be set by calling it with an argument, and can be read
by calling it with no arguments.
```lua
local function Text(p: {
Text: () -> string
})
return create "TextLabel" {
Text = p.Text
}
end
local text = source "hi"
Text {
Text = text
}
count(count() + 1) -- increment count by 1
```
Each call of `Counter {}` will create a new counter, each maintaining their
own count.
When you assign a function to a non-event property, Vide will immediately run it
and check what sources were read from. When updating those sources again after,
this function will be re-ran and its return value applied to the property.
This is known as *binding* properties.
This allows you as the programmer to not need to manually update UI as the state
of your program changes. You just define how the data maps to UI, and Vide's
reactive system will automatically update any properties depending on sources
that are updated.

View file

@ -1,102 +0,0 @@
# Derived Source
You can create new sources from existing sources. This is known as *deriving
sources*.
A function that wraps a source effectively becomes a new source. If a source
used inside a function is updated, the whole function can be re-ran to recompute
its value.
```lua
local count = source(0)
local function text()
return "count: " .. count()
end
create "TextLabel" {
Text = text
}
```
Sometimes when using expensive computations to derive state, you only want to
recalculate it once when a source state has changed
If you wrap a source with a regular function, its value will be recomputed
every time you call that function.
[`derive()`](../../api/reactivity-core.md#derive) accepts a functions whose
return value will be cached, so that subsequent calls of this derived source
will return the same cached value until one of its input sources have changed.
```lua
local derive = vide.derive
```
```lua
local count = source(0)
local factorial = derive(function()
local n = 1
for i = 2, count() do
n *= i
end
return n
end)
```
This can improve performance in cases where a source is read from multiple times
between recalculations, like in the example below:
```lua
create "TextLabel" {
Text = function()
return "factorial squared: " .. factorial() * factorial()
end
}
count(3) -- displays "factorial squared: 36"
count(4) -- displays "factorial squared: 576"
```
Vide knows what sources are being depended on by immediately running the
callback when deriving or binding sources. If a source is in a function but is
never referenced the first time it runs, Vide will not know to rerun the
function if that source changes.
An example to watch out for is when using sources within branches:
```lua
local condition = source(true)
local count1 = source(0)
local count2 = source(0)
local text = function()
if condition() then
return "text: " .. count1()
else
return "text: " .. count2()
end
end
```
In the above case, only `count1` will be referenced, meaning `text` will not be
aware of `count2` even if the condition is later set to false.
All sources to be tracked must be referenced the first time the function runs.
```lua
local condition = source(true)
local count1 = source(0)
local count2 = source(0)
local text = function()
local c1 = count1()
local c2 = count2()
if condition() then
return "text: " .. c1
else
return "text: " .. c2
end
end
```

View file

@ -0,0 +1,45 @@
# Effect
An effect is a function that is run anytime a source updates. They are called
effects because they can produce side-effects when reacting to source changes.
Effects are created using `effect()`.
```lua
local vide = require(vide)
local source = vide.source
local effect = vide.effect
local function Counter()
local count = source(0)
effect(function()
print("count has updated to: " .. count())
end)
return create "TextButton" {
Position = UDim2.fromOffset(300, 300),
Size = UDim2.fromOffset(200, 50),
Text = count,
Activated = function()
count(count() + 1)
end
}
end
mount(function() return create "ScreenGui" { Counter {} } end, game.StarterGui)
```
This will print to the terminal anytime the count is changed.
`effect()` creates an explicit side-effect. There are other side-effects in the
above code sample. The setting of `Text = count` creates another side-effect;
the updating of the Text property anytime the count is changed.
All observable changes to the user are considered to be side-effects of the
reactive system.
You should not update other sources using an effect. Improper usage can lead to
unecessary updates and infinite loops.

View file

@ -0,0 +1,76 @@
# Derived Source
You can create new sources from existing sources. This is known as *deriving
sources*.
A function that wraps a source effectively becomes a new source. If a source
used inside a function is updated, the whole function can be re-ran to recompute
its value.
```lua
local vide = require(vide)
local source = vide.source
local function Counter()
local count = source(0)
local function doubled()
return count() * 2
end
return create "TextButton" {
Position = UDim2.fromOffset(300, 300),
Size = UDim2.fromOffset(200, 50),
Text = doubled,
Activated = function()
count(count() + 1)
end
}
end
mount(function() return create "ScreenGui" { Counter {} } end, game.StarterGui)
```
Now the counter will increment in 2s each time it is clicked.
Sometimes when using expensive computations to derive state, you only want to
recalculate it once when a source state has changed. Although not needed in
most cases, you can use `derive()` to create a new source that will cache its
value, only recomputing when an input source has changed.
```lua
local vide = require(vide)
local source = vide.source
local derive = vide.derive
local function Counter()
local count = source(0)
local factorial = derive(function()
local n = 1
for i = 2, count() do
n *= i
end
return n
end)
return create "TextButton" {
Position = UDim2.fromOffset(300, 300),
Size = UDim2.fromOffset(200, 50),
Text = function()
return factorial() + factorial() + factorial()
end,
Activated = function()
count(count() + 1)
end
}
end
```
This can improve performance in cases where a source is read from multiple times
between recalculations. In the above example, the factorial is only ever
calculated once each time the count changes.

View file

@ -1,66 +0,0 @@
# Table Source
Vide has specific functions for dealing with sources that store a table value.
Often, you will have a table of values that will be displayed in a similar
manner. Rather than manually looping over each value to generate a corresponding
UI element, Vide provides functions `indexes()` and `values()` to do this for
you.
`indexes()` maps each *index* in a table to a UI element.
```lua
local names = source { "a", "b", "c" }
local elements = indexes(names, function(name, i)
return create "TextLabel" {
Text = function()
return "Name: " .. name()
end,
LayoutOrder = i
}
end)
```
What happens here is the given callback is only ever ran *once* for each index
in the table. The callback receives two arguments, a *source* containing the
index's value and then the index itself.
Anytime the value at a corresponding index changes, the source for that index
value is updated, causing the UI element depending on it to update too.
`values()` behaves similarly, except it maps each *value* in a table to a UI
element.
```lua
type Item = {
Name: string,
Icon: number
}
local items = source({} :: Array<Item>)
local elements = values(items, function(item, i)
return create "ImageLabel" {
Image = "rbxassetid://" .. item.Icon,
LayoutOrder = i
}
end)
```
The callback is again only ever ran *once* for each value in the table. The
callback receives two arguments, a value in the table and then a *source*
containing the value's corresponding index.
Any time a value in a table changes index, the source for that value is updated,
causing the UI element position to change.
In certain cases `values()` can cause less recalculation and rerenders than
`indexes()` like when items are re-arranged and shifted within a table.
It is important that each value in a table is unique when using `values()`,
and for this reason always using `indexes()` if a table contains primitive
values.
Both `indexes()` and `values()` return an array of all mapped UI elements.

View file

@ -0,0 +1,40 @@
# Cleanup
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 register a cleanup callback for the next time the reactive scope
it is called in re-runs.
```lua
local vide = require(vide)
local source = vide.source
local cleanup = vide.cleanup
local function Timer()
local count = source(0)
local con = game:GetService("RunService").Heartbeat:Connect(function(dt)
count(count() + dt)
end)
cleanup(function()
con:Disconnect()
end)
return create "TextButton" {
Position = UDim2.fromOffset(300, 300),
Size = UDim2.fromOffset(200, 50),
Text = function()
return "seconds: " .. count()
end,
}
end
mount(function() return create "ScreenGui" { Timer {} } end, game.StarterGui)
```
In the above example, this allows us to disconnect the heartbeat connection
when the timer component is destroyed, whether that is from unmounting the app
or if it is dynamically created by a control-flow function, which will be
covered next.

View file

@ -0,0 +1,95 @@
# Control Flow
Eventually you will need a way to dynamically create and destroy UI elements
resulting from state changes. Vide provides functions to help you do this,
known as *control flow* functions.
These functions return a new source, which holds the instances to be displayed.
These sources can be assigned as children, meaning the displayed children
will update when the input source updates.
One of these functions is `switch()`, used to conditionally show one of a set of
components.
```lua
local vide = require(vide)
local source = vide.source
local switch = vide.switch
local function ToggleButton(p: {
Text: string,
Toggle: (boolean) -> boolean
})
return create "TextButton" {
Size = UDim2.fromOffset(300, 300),
Text = p.Text,
Activated = function()
p.Toggle(not p.Toggle())
end
}
end
local loggedIn = source(false)
local function LoginMenu()
return Frame {
switch(loggedIn) {
[true] = function()
return ToggleButton { Text = "Log out", Toggle = loggedIn }
end,
[false] = function()
return ToggleButton { Text = "Log in", Toggle = loggedIn }
end
}
}
end
mount(function() return create "ScreenGui" { LoginMenu {} } end, game.StarterGui)
```
Above is an example of using a switch to create a login menu. Each time
`loggedIn` toggles, the current button will be destroyed, and a new button
created, which the text to represent the current action, to log in or log out.
Another control flow function, `indexes()`, is used to create elements from an
input table.
Often, you will have a table of values that will be displayed in a similar
manner. Rather than manually looping over each value to generate a corresponding
UI element, `indexes()` can autmatically run a transform function for each
index and value, generating a UI element.
```lua
local todoList = {
"Finish the crash course",
"Star vide's GitHub"
}
local elements = indexes(todoList, function(todo, i)
return create "TextLabel" {
Text = function()
return i .. ": " .. todo()
end,
LayoutOrder = i
}
end)
mount(function()
return create "ScreenGui" {
create "UIListLayout" {}, elements
}
end, game.StarterGui)
```
For each unique index in the passed table, the transform function will be called
with 1. a source containing the value of the index, 2. the index itself.
When the value at an index is changed, the function is not reran. Instead, the
given source is updated instead.
`indexes()` is said to map each *index* in a table to a UI element, each index
has a single corresponding element.
An element is only destroyed if the value of an index is set to `nil`.