mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
Update crash course
This commit is contained in:
parent
520b0cca32
commit
bb08e2e97a
15 changed files with 272 additions and 160 deletions
|
|
@ -41,13 +41,15 @@ export default defineConfig({
|
|||
{ text: "Introduction", link: "/tut/crash-course/1-introduction" },
|
||||
{ text: "Element Creation", link: "/tut/crash-course/2-creation" },
|
||||
{ text: "Components", link: "/tut/crash-course/3-components" },
|
||||
{ text: "Source", link: "/tut/crash-course/4-source" },
|
||||
{ text: "Effect", link: "/tut/crash-course/5-effect" },
|
||||
{ text: "Derived Source", link: "/tut/crash-course/6-derived-source" },
|
||||
{ text: "Cleanup", link: "/tut/crash-course/7-cleanup" },
|
||||
{ text: "Control Flow", link: "/tut/crash-course/8-control-flow" },
|
||||
{ text: "Property Nesting", link: "/tut/crash-course/9-property-nesting" },
|
||||
{ text: "Actions", link: "/tut/crash-course/10-actions" },
|
||||
{ text: "Sources", link: "/tut/crash-course/4-source" },
|
||||
{ text: "Effects", link: "/tut/crash-course/5-effect" },
|
||||
{ text: "Stateful Components", link: "/tut/crash-course/6-stateful-component" },
|
||||
{ text: "Property Binding", link: "/tut/crash-course/7-property-binding" },
|
||||
{ text: "Cleanup", link: "/tut/crash-course/8-cleanup" },
|
||||
{ text: "Control Flow", link: "/tut/crash-course/9-control-flow" },
|
||||
{ text: "Property Nesting", link: "/tut/crash-course/10-property-nesting" },
|
||||
{ text: "Actions", link: "/tut/crash-course/11-actions" },
|
||||
{ text: "Strict Mode", link: "/tut/crash-course/12-strict-mode" },
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -16,11 +16,7 @@ worrying about manually updating UI instances.
|
|||
Some of the main focuses behind Vide's design choices:
|
||||
|
||||
- Concise syntax to reduce verbosity as much as possible.
|
||||
- Reducing the amount of imports needed for usage by leveraging Luau's syntax
|
||||
and semantics.
|
||||
- Being completely typecheckable.
|
||||
- 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.
|
||||
|
|
@ -36,7 +32,6 @@ 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" }
|
||||
|
|
|
|||
|
|
@ -77,7 +77,8 @@ be parented.
|
|||
|
||||
```lua
|
||||
type Children = {
|
||||
Children = Array<Instance>
|
||||
-- allows us to also optionally pass a source that returns an array of children instead
|
||||
Children = Array<Instance> | () -> Array<Instance>
|
||||
}
|
||||
|
||||
local function List(props: Children & Layout)
|
||||
|
|
@ -107,8 +108,8 @@ properties, this can be used to create overridable default properties.
|
|||
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),
|
||||
|
|
@ -24,19 +24,32 @@ Actions can be wrapped with functions to re-use specific behaviors. Below is
|
|||
an example of an action used to listen for property changes:
|
||||
|
||||
```lua
|
||||
local action = vide.action
|
||||
local cleanup = vide.cleanup
|
||||
|
||||
local function changed(property: string, callback: (new) -> ())
|
||||
return action(function(instance)
|
||||
instance:GetPropertyChangedSignal(property):Connect(function()
|
||||
local con = instance:GetPropertyChangedSignal(property):Connect(function()
|
||||
callback(instance[property])
|
||||
end)
|
||||
|
||||
-- remember to clean up the connection when the reactive scope the action
|
||||
-- is ran in is destroyed, so the instance can be garbage collected
|
||||
cleanup(function()
|
||||
con:Disconnect()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
local output = source ""
|
||||
|
||||
create "TextBox" {
|
||||
local instance = create "TextBox" {
|
||||
changed("Text", output)
|
||||
}
|
||||
|
||||
instance.Text = "foo"
|
||||
|
||||
print(output()) -- "foo"
|
||||
```
|
||||
|
||||
The source `output` will be updated with the new property value any time it is
|
||||
9
docs/tut/crash-course/12-strict-mode.md
Normal file
9
docs/tut/crash-course/12-strict-mode.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Strict Mode
|
||||
|
||||
While developing UI with Vide, you should use Vide's strict mode, which can
|
||||
be set with `vide.strict = true` once when you first require Vide. Strict mode
|
||||
will add extra safety checks and emit better error traces, particularly when
|
||||
errors occur in property bindings.
|
||||
|
||||
A full list of what strict mode will do can be found
|
||||
[here](../../api/strict-mode).
|
||||
|
|
@ -9,7 +9,6 @@ Luau allows us to omit parentheses `()` when calling functions with string or
|
|||
table literals which Vide takes advantage of for brevity.
|
||||
|
||||
```lua
|
||||
local vide = require(vide)
|
||||
local mount = vide.mount
|
||||
local create = vide.create
|
||||
|
||||
|
|
@ -56,3 +55,9 @@ create "Frame" {
|
|||
UDim2 = { 0.5, 0, 0.5, 0 }
|
||||
}
|
||||
```
|
||||
|
||||
When creating an instance with no properties, it is important to not forget to
|
||||
actually call the constructor: `create "Frame" {}` and not `create "Frame"`.
|
||||
To be clear, `create "Frame"` returns a *function* which is a constructor for
|
||||
that class, not an instance of that class. This would result in you attempting
|
||||
to parent a function instead of an instance which is not the correct behavior.
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ By using components you can make your application more modular and better
|
|||
organized.
|
||||
|
||||
```lua [Button.luau]
|
||||
local vide = require(vide)
|
||||
local create = vide.create
|
||||
|
||||
local function Button(props: {
|
||||
|
|
@ -28,7 +27,6 @@ return Button
|
|||
```
|
||||
|
||||
```lua [App.luau]
|
||||
local vide = require(vide)
|
||||
local mount = vide.mount
|
||||
local create = vide.create
|
||||
|
||||
|
|
@ -56,11 +54,11 @@ being reused across files.
|
|||
A single parameter `props` is used to pass properties to the component.
|
||||
|
||||
Components allow you to *encapsulate* behavior. You can only modify the
|
||||
component in ways that you allow in the component.
|
||||
component in ways that you allow in the component, through the `props` parameter.
|
||||
|
||||
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.
|
||||
To create a new button all you must do is call the `Button` function, passing in
|
||||
values through props. This saves having to create and set every property each
|
||||
time. Also, when updating the button component in future, any changes to the
|
||||
button file will be seen anywhere the button is used in your app.
|
||||
|
||||
This can be extended to much more complicated UI.
|
||||
|
|
|
|||
|
|
@ -1,31 +1,15 @@
|
|||
# Source
|
||||
|
||||
*Sources* in Vide are special objects that store a single value. They are the
|
||||
core of reactivity in Vide, as updates to a source can automatically update
|
||||
properties or other sources depending on that source.
|
||||
core of reactivity in Vide. Each source represents a source of data, and they
|
||||
can be composed and derived to create new sources of data.
|
||||
|
||||
A source in Vide can be created using `source()`.
|
||||
|
||||
```lua
|
||||
local vide = require(vide)
|
||||
local source = vide.source
|
||||
|
||||
local function Counter()
|
||||
local count = source(0)
|
||||
|
||||
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)
|
||||
local count = source(0)
|
||||
```
|
||||
|
||||
The value passed to `source()` is the initial value of the source.
|
||||
|
|
@ -37,15 +21,23 @@ by calling it with no arguments.
|
|||
count(count() + 1) -- increment count by 1
|
||||
```
|
||||
|
||||
Each call of `Counter {}` will create a new counter, each maintaining their
|
||||
own count.
|
||||
Sources can be *derived* by wrapping them in functions. A wrapped source
|
||||
effectively becomes a new source.
|
||||
|
||||
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.
|
||||
```lua
|
||||
local count = source(0)
|
||||
|
||||
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.
|
||||
local text = function()
|
||||
return "count: " .. tostring(count())
|
||||
end
|
||||
|
||||
print(text()) -- "count: 0"
|
||||
count(1)
|
||||
print(text()) -- "count: 1"
|
||||
```
|
||||
|
||||
You may be wondering why we are using sources instead of plain variables to do
|
||||
this. The reason is that Vide has an entire reactive system based on sources.
|
||||
You can write functions to automatically run each time a source is updated. This
|
||||
can be to update properties, create new instances, print to the terminal, etc.
|
||||
How this is done will be covered next.
|
||||
|
|
|
|||
|
|
@ -1,45 +1,75 @@
|
|||
# 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 functions that are ran in response to source updates. They are
|
||||
alled effects because they cause *side-effects* when reacting to source updates.
|
||||
|
||||
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)
|
||||
local count = source(0)
|
||||
|
||||
effect(function()
|
||||
print("count has updated to: " .. count())
|
||||
end)
|
||||
effect(function()
|
||||
print("count: " .. 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)
|
||||
-- "count: 0" printed
|
||||
count(1)
|
||||
-- "count: 1" printed
|
||||
```
|
||||
|
||||
This will print to the terminal anytime the count is changed.
|
||||
The callback given to `effect()` is ran in a *reactive-scope*. Any source read
|
||||
from inside a reactive scope will be tracked, so that if any of those sources
|
||||
update, the effect will be re-ran too.
|
||||
|
||||
`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.
|
||||
The callback is first ran immediately inside the `effect()` call to initially
|
||||
figure out what sources are being used.
|
||||
|
||||
All observable changes to the user are considered to be side-effects of the
|
||||
reactive system.
|
||||
Effects also work with derived sources, it doesn't matter how deeply nested a
|
||||
source is.
|
||||
|
||||
```lua
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
|
||||
local count = source(1)
|
||||
|
||||
local doubled = function()
|
||||
return count() * 2
|
||||
end
|
||||
|
||||
effect(function()
|
||||
print("doubled count: " .. doubled())
|
||||
end)
|
||||
|
||||
-- "doubled count: 2" printed
|
||||
count(2)
|
||||
-- "doubled count: 4" printed
|
||||
```
|
||||
|
||||
Derived sources should be a *pure computation*. A pure computation is one where
|
||||
the same input will always produce the same output.
|
||||
|
||||
All observable changes to the user are considered to be side-effects of pure
|
||||
computations.
|
||||
|
||||
Sources, derived sources, and effects form what is called a *reactive graph*.
|
||||
In the above example a graph `count -> doubled -> effect` is formed. Anywhere
|
||||
an update occures, everything further down the graph is updated.
|
||||
|
||||
You should not update other sources using an effect. Improper usage can lead to
|
||||
unecessary updates and infinite loops.
|
||||
a cyclic loop in the graph, causing an infinite loop when it tries to update.
|
||||
Sources should be derived instead.
|
||||
|
||||
## Root Reactive Scopes
|
||||
|
||||
Effects must be created within another reactive scope. This is so that the
|
||||
effect itself can be tracked and later freed when the parent reactive scope is
|
||||
destroyed, such as from unmounting an app. The example code above will not
|
||||
actually work unless it is ran inside a root reactive scope, such as one created
|
||||
by `vide.mount(function)`. This generally isn't a concern since you can assume
|
||||
that all your components will be created within a single `mount()` call, which
|
||||
happens only once at the top level, where you put together your UI and parent it
|
||||
to a ScreenGUI.
|
||||
|
|
|
|||
|
|
@ -1,76 +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 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.
|
||||
66
docs/tut/crash-course/6-stateful-component.md
Normal file
66
docs/tut/crash-course/6-stateful-component.md
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# Stateful Component
|
||||
|
||||
A stateful component is a component that stores and displays some data.
|
||||
|
||||
Stateful components in Vide are created using sources and effects - sources to
|
||||
store the data, and effects to display the data.
|
||||
|
||||
```lua
|
||||
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 count
|
||||
end
|
||||
```
|
||||
|
||||
Above is an example of a counter component, that when clicked, will increment
|
||||
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 scope of the component.
|
||||
|
||||
External sources can also be passed into components for them to use.
|
||||
|
||||
```lua
|
||||
local function Counter(props: { count: () -> number })
|
||||
local count = props.count
|
||||
|
||||
local instance = create "TextButton" {
|
||||
Activated = function()
|
||||
count(count() + 1)
|
||||
end
|
||||
}
|
||||
|
||||
effect(function()
|
||||
instance.Text = "count: " .. count()
|
||||
end)
|
||||
|
||||
return count
|
||||
end
|
||||
|
||||
local count = source(0)
|
||||
|
||||
Counter {
|
||||
count = count
|
||||
}
|
||||
|
||||
count(1) -- the Counter component will update to display this count
|
||||
```
|
||||
|
||||
Sources can be created internally or passed in from externally, there are no
|
||||
restrictions on how they are used as long as the effect is created within a
|
||||
reactive scope so that it can be tracked.
|
||||
64
docs/tut/crash-course/7-property-binding.md
Normal file
64
docs/tut/crash-course/7-property-binding.md
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# Property Binding
|
||||
|
||||
Explicitly creating effects to update properties can become verbose when there
|
||||
are a lot of properties to update. Vide provides a way to *implicitly* create
|
||||
an effect to update properties on source update. This is also known as
|
||||
*property binding*, as a property is binded to reflect some data.
|
||||
|
||||
```lua
|
||||
local create = vide.create
|
||||
local source = vide.source
|
||||
|
||||
local function Counter()
|
||||
local count = source(0)
|
||||
|
||||
return create "TextButton" {
|
||||
Text = function()
|
||||
return "count: " .. count()
|
||||
end,
|
||||
|
||||
Activated = function()
|
||||
count(count() + 1)
|
||||
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 function will implicitly create a side-effect to update that property anytime
|
||||
a dependent source 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 depended on.
|
||||
|
||||
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.
|
||||
|
||||
## Children Binding
|
||||
|
||||
Children can also be set in a similar manner.
|
||||
|
||||
```lua
|
||||
local items = source {
|
||||
create "TextLabel" { Text = "A" }
|
||||
}
|
||||
|
||||
local function List(props: { children: () -> { Instance } })
|
||||
return create "Frame" {
|
||||
create "UIListLayout" {},
|
||||
props.children
|
||||
}
|
||||
end
|
||||
|
||||
local list = List { children = items } -- creates a list with a single 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".
|
||||
```
|
||||
|
|
@ -6,7 +6,7 @@ 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)
|
||||
locla mount = vide.mount
|
||||
local source = vide.source
|
||||
local cleanup = vide.cleanup
|
||||
|
||||
|
|
@ -26,15 +26,23 @@ local function Timer()
|
|||
Size = UDim2.fromOffset(200, 50),
|
||||
|
||||
Text = function()
|
||||
return "seconds: " .. count()
|
||||
return "seconds: " .. math.floor(count())
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
mount(function() return create "ScreenGui" { Timer {} } end, game.StarterGui)
|
||||
local unmount = mount(Timer)
|
||||
|
||||
unmount() -- all registered cleanups are ran, heartbeat connection stopped
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
On a related note: the reason why `mount()` is used to create your app, is so
|
||||
that any top-level components that need to be cleaned up, can be cleaned up
|
||||
when the app is later unmounted, since `mount()` runs in a reactive-scope to
|
||||
track `cleanup()` calls. Vide's entire reactive system is independent from the
|
||||
life-time of instances; instances are just a side-effect of the reactive system.
|
||||
|
|
@ -52,6 +52,10 @@ 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.
|
||||
|
||||
The callbacks given to control flow functions are ran in a new reactive-scope,
|
||||
so any cleanups registered will be ran when the input is changed and a new
|
||||
output is created.
|
||||
|
||||
Another control flow function, `indexes()`, is used to create elements from an
|
||||
input table.
|
||||
|
||||
1
todo.md
1
todo.md
|
|
@ -12,3 +12,4 @@
|
|||
- optimize `indexes()` double-diffing
|
||||
- define behavior of deriving a source within a derived source
|
||||
- review destruction of node under a root that has a child in another root
|
||||
- check in strict mode for forgetting to call constructor `create(class)`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue