mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
This commit is contained in:
parent
da40e05fd8
commit
d353f71619
10 changed files with 214 additions and 160 deletions
|
|
@ -27,21 +27,21 @@ Some of the main focuses behind Vide's design choices:
|
||||||
|
|
||||||
## Structure Of A Vide App
|
## Structure Of A Vide App
|
||||||
|
|
||||||
The entry point for all Vide apps is the `root()` function. This function
|
The entry point for all Vide apps is the `mount()` function. This function
|
||||||
sets up Vide's reactivity system and allows proper disposal of your app. It
|
sets up Vide's reactivity system. It takes and calls a function that should
|
||||||
takes and calls a function that should create your entire app, then returns the
|
create your entire app, and will apply its result to a target.
|
||||||
result.
|
|
||||||
|
|
||||||
In Vide, your app should be composed of functions, each function creates a
|
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
|
specific part of your app, and can be reused if needed. These functions are
|
||||||
called *components*.
|
called *components*.
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
|
|
||||||
local function App()
|
local function App()
|
||||||
return create "ScreenGui" {
|
return create "ScreenGui" {
|
||||||
create "TextLabel" { Text = "hi" }
|
create "TextLabel" { Text = "hi" }
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
root(App).Parent = game.StarterGui
|
mount(App, game.StarterGui)
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,20 @@
|
||||||
# Creating UI
|
# Creating UI
|
||||||
|
|
||||||
Instances are created using [`create()`](../../api/creation.md#create).
|
Instances are created using `create()`.
|
||||||
|
|
||||||
```lua
|
|
||||||
local vide = require(vide)
|
|
||||||
local create = vide.create
|
|
||||||
```
|
|
||||||
|
|
||||||
`create()` returns a constructor for a class which then takes a table of
|
`create()` returns a constructor for a class which then takes a table of
|
||||||
properties to assign when creating a new instance for that class.
|
properties to assign when creating a new instance for that class.
|
||||||
|
|
||||||
Luau allows us to omit parentheses `()` when calling functions with string or
|
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
|
```lua
|
||||||
local frame = create "Frame" {
|
local vide = require(vide)
|
||||||
Name = "Background",
|
local mount = vide.mount
|
||||||
Position = UDim2.fromScale(0.5, 0.5)
|
local create = vide.create
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
String keys are treated as properties and integer keys are treated as child
|
|
||||||
instances.
|
|
||||||
|
|
||||||
```lua
|
|
||||||
create "ScreenGui" {
|
|
||||||
Parent = game.StarterGui,
|
|
||||||
|
|
||||||
|
local function App()
|
||||||
|
return create "ScreenGui" {
|
||||||
create "Frame" {
|
create "Frame" {
|
||||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||||
Position = UDim2.fromScale(0.5, 0.5),
|
Position = UDim2.fromScale(0.5, 0.5),
|
||||||
|
|
@ -36,27 +24,31 @@ create "ScreenGui" {
|
||||||
Text = "hi"
|
Text = "hi"
|
||||||
},
|
},
|
||||||
|
|
||||||
create"TextLabel" {
|
create "TextLabel" {
|
||||||
Text = "bye"
|
Text = "bye"
|
||||||
}
|
},
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
To connect to an event, just assign the event property a function.
|
create "TextButton" {
|
||||||
|
Text = "click me",
|
||||||
|
|
||||||
All event arguments are passed into the function.
|
|
||||||
|
|
||||||
```lua
|
|
||||||
create "TextButton" {
|
|
||||||
Activated = function()
|
Activated = function()
|
||||||
print "clicked!"
|
print "clicked!"
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
mount(App, game.StarterGui)
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also use a form of aggregate initialization to create datatypes instead
|
Assign a value to a string key to set a property, and assign a value to a
|
||||||
of explicitly typing out the class name and constructor.
|
number key to set a child. Events can be connected to by assigning a function
|
||||||
|
to a string key.
|
||||||
|
|
||||||
|
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
|
```lua
|
||||||
create "Frame" {
|
create "Frame" {
|
||||||
|
|
@ -64,7 +56,3 @@ create "Frame" {
|
||||||
UDim2 = { 0.5, 0, 0.5, 0 }
|
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.
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ return Button
|
||||||
|
|
||||||
```lua [App.luau]
|
```lua [App.luau]
|
||||||
local vide = require(vide)
|
local vide = require(vide)
|
||||||
|
local mount = vide.mount
|
||||||
local create = vide.create
|
local create = vide.create
|
||||||
|
|
||||||
local Button = require(Button)
|
local Button = require(Button)
|
||||||
|
|
@ -46,7 +47,7 @@ local function App()
|
||||||
}
|
}
|
||||||
end
|
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,
|
Above is a simple example of a button component with a set color and size,
|
||||||
|
|
|
||||||
|
|
@ -4,35 +4,18 @@
|
||||||
core of reactivity in Vide, as updates to a source can automatically update
|
core of reactivity in Vide, as updates to a source can automatically update
|
||||||
properties or other sources depending on that source.
|
properties or other sources depending on that source.
|
||||||
|
|
||||||
A source in Vide can be created using
|
A source in Vide can be created using `source()`.
|
||||||
[`source()`](../../api/reactivity-core.md#source).
|
|
||||||
|
|
||||||
```lua
|
```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 vide = require(vide)
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
|
|
||||||
local function Counter(props: { Position: UDim2 })
|
local function Counter()
|
||||||
local count = source(0)
|
local count = source(0)
|
||||||
|
|
||||||
return create "TextButton" {
|
return create "TextButton" {
|
||||||
Position = props.Position,
|
Position = UDim2.fromOffset(300, 300),
|
||||||
Size = UDim2.new(200, 50),
|
Size = UDim2.fromOffset(200, 50),
|
||||||
|
|
||||||
Text = count,
|
Text = count,
|
||||||
|
|
||||||
|
|
@ -41,18 +24,28 @@ local function Counter(props: { Position: UDim2 })
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
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
|
The value passed to `source()` is the initial value of the source.
|
||||||
independent count.
|
|
||||||
|
|
||||||
Vide detects when you assign a function to a property. This is known
|
The value of a source can be set by calling it with an argument, and can be read
|
||||||
as *binding* and doing so will cause the property to *automatically* update
|
by calling it with no arguments.
|
||||||
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
|
```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
|
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
|
reactive system will automatically update any properties depending on sources
|
||||||
are changed.
|
that are updated.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -8,95 +8,69 @@ used inside a function is updated, the whole function can be re-ran to recompute
|
||||||
its value.
|
its value.
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local count = source(0)
|
local vide = require(vide)
|
||||||
|
local source = vide.source
|
||||||
|
|
||||||
local function text()
|
local function Counter()
|
||||||
return "count: " .. count()
|
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
|
end
|
||||||
|
|
||||||
create "TextLabel" {
|
mount(function() return create "ScreenGui" { Counter {} } end, game.StarterGui)
|
||||||
Text = text
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Now the counter will increment in 2s each time it is clicked.
|
||||||
|
|
||||||
Sometimes when using expensive computations to derive state, you only want to
|
Sometimes when using expensive computations to derive state, you only want to
|
||||||
recalculate it once when a source state has 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
|
||||||
If you wrap a source with a regular function, its value will be recomputed
|
value, only recomputing when an input source has changed.
|
||||||
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
|
```lua
|
||||||
|
local vide = require(vide)
|
||||||
|
local source = vide.source
|
||||||
local derive = vide.derive
|
local derive = vide.derive
|
||||||
```
|
|
||||||
|
|
||||||
```lua
|
local function Counter()
|
||||||
local count = source(0)
|
local count = source(0)
|
||||||
|
|
||||||
local factorial = derive(function()
|
local factorial = derive(function()
|
||||||
local n = 1
|
local n = 1
|
||||||
for i = 2, count() do
|
for i = 2, count() do
|
||||||
n *= i
|
n *= i
|
||||||
end
|
end
|
||||||
return n
|
return n
|
||||||
end)
|
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
|
This can improve performance in cases where a source is read from multiple times
|
||||||
between recalculations, like in the example below:
|
between recalculations. In the above example, the factorial is only ever
|
||||||
|
calculated once each time the count changes.
|
||||||
```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
|
|
||||||
```
|
|
||||||
|
|
|
||||||
40
docs/tut/crash-course/7-cleanup.md
Normal file
40
docs/tut/crash-course/7-cleanup.md
Normal 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.
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# Table Source
|
# Control Flow
|
||||||
|
|
||||||
Vide has specific functions for dealing with sources that store a table value.
|
Vide has specific functions for dealing with sources that store a table value.
|
||||||
|
|
||||||
16
src/mount.luau
Normal file
16
src/mount.luau
Normal 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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue