This commit is contained in:
aaron 2023-09-12 23:12:24 +01:00
parent da40e05fd8
commit d353f71619
10 changed files with 214 additions and 160 deletions

View file

@ -27,21 +27,21 @@ Some of the main focuses behind Vide's design choices:
## Structure Of A Vide App
The entry point for all Vide apps is the `root()` function. This function
sets up Vide's reactivity system and allows proper disposal of your app. It
takes and calls a function that should create your entire app, then returns the
result.
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
root(App).Parent = game.StarterGui
mount(App, game.StarterGui)
```

View file

@ -1,62 +1,54 @@
# Creating UI
Instances are created using [`create()`](../../api/creation.md#create).
```lua
local vide = require(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

@ -29,6 +29,7 @@ return Button
```lua [App.luau]
local vide = require(vide)
local mount = vide.mount
local create = vide.create
local Button = require(Button)
@ -46,7 +47,7 @@ local function App()
}
end
root(App).Parent = game.StarterGui
mount(App, game.StarterGui)
```
Above is a simple example of a button component with a set color and size,

View file

@ -4,35 +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 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 [Counter.luau]
local vide = require(vide)
local source = vide.source
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,
@ -41,18 +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.
The value of a source can be set by calling it with an argument, and can be read
by calling it with no arguments.
This allows you as the programmer to not need to manually update GUI as the state
```lua
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 surgically update any properties depending on sources that
are changed.
reactive system will automatically update any properties depending on sources
that are updated.

View file

@ -0,0 +1,42 @@
# 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.

View file

@ -8,95 +8,69 @@ used inside a function is updated, the whole function can be re-ran to recompute
its value.
```lua
local count = source(0)
local vide = require(vide)
local source = vide.source
local function text()
return "count: " .. count()
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
create "TextLabel" {
Text = text
}
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
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.
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
```
```lua
local count = source(0)
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)
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, 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 effect 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
```
between recalculations. In the above example, the factorial is only ever
calculated once each time the count changes.

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

@ -1,4 +1,4 @@
# Table Source
# Control Flow
Vide has specific functions for dealing with sources that store a table value.

16
src/mount.luau Normal file
View file

@ -0,0 +1,16 @@
if not game then script = require "test/relative-string" end
local root = require(script.Parent.root)
local apply = require(script.Parent.apply)
local function mount<T>(app: () -> T, target: Instance?): () -> ()
local _, destroy = root(function()
local result = app()
if target then apply(target, { result }) end
return nil
end)
return destroy
end
return mount