Update docs

This commit is contained in:
Aaron Smith 2023-09-28 16:39:25 +01:00
parent 9b6be14441
commit 46937c6979
16 changed files with 157 additions and 116 deletions

View file

@ -22,8 +22,9 @@ Currently, strict mode will:
6. Checks for duplicate nested properties at same depth. 6. Checks for duplicate nested properties at same depth.
7. Better error reporting and stack traces + creation traces of property bindings. 7. Better error reporting and stack traces + creation traces of property bindings.
By rerunning sources and effects, any side-effects are made more apparent. By rerunning derived sources and effects twice each time they update,it helps
This also helps ensure that cleanups are being handled correctly. ensure that derived source computations are pure, and that any
cleanups made in derived sources or effects are done correctly.
Accidental yielding within reactive scopes can break Vide's reactive graph, Accidental yielding within reactive scopes can break Vide's reactive graph,
which strict mode can catch. which strict mode can catch.

View file

@ -60,7 +60,7 @@ This code will produce a graph that looks like so:
} }
}}%% }}%%
flowchart graph
subgraph root subgraph root
forename & surname --> name forename & surname --> name
name --> effect name --> effect
@ -145,7 +145,7 @@ This code produces a graph like so:
} }
}}%% }}%%
flowchart LR graph LR
subgraph root subgraph root
counters --> indexes counters --> indexes
@ -208,7 +208,7 @@ The above code produces a graph like so:
} }
}}%% }}%%
flowchart LR graph LR
subgraph root subgraph root
direction LR direction LR
show show

View file

@ -5,6 +5,8 @@ Vide.
Vide is heavily inspired by [Solid](https://www.solidjs.com/). Vide is heavily inspired by [Solid](https://www.solidjs.com/).
This tutorial assumes familiarity with Luau and Roblox GUI.
## Why Vide? ## Why Vide?
Creating UI is a slow and tedious process. The purpose of Vide is to make UI Creating UI is a slow and tedious process. The purpose of Vide is to make UI

View file

@ -6,7 +6,7 @@ is used to register a cleanup callback for the next time the reactive scope
it is called in re-runs. it is called in re-runs.
```lua ```lua
locla mount = vide.mount local mount = vide.mount
local source = vide.source local source = vide.source
local cleanup = vide.cleanup local cleanup = vide.cleanup
@ -60,11 +60,11 @@ The reactive graph for the above example:
} }
}}%% }}%%
flowchart graph
subgraph root subgraph mount
direction LR direction LR
cleanup([cleanup]) ~~~ count cleanup([cleanup]) ~~~ count
count --> bind[text binding] count --> bind["effect (text binding)"]
end end
``` ```

View file

@ -11,7 +11,7 @@ will update when the input source updates.
Control flow functions are special, because they run their components in a new Control flow functions are special, because they run their components in a new
reactive scope, which can be destroyed independently of the reactive scope that reactive scope, which can be destroyed independently of the reactive scope that
called the control flow function itself. This means that parts of your app can called the control flow function itself. This means that parts of your app can
be independently created then destroyed and cleaned. be independently created then destroyed.
## show() ## show()
@ -78,7 +78,7 @@ The reactive graph for the above example:
} }
}}%% }}%%
flowchart graph
subgraph root["mount() scope"] subgraph root["mount() scope"]
direction LR direction LR
@ -91,8 +91,11 @@ subgraph root ["mount() scope"]
end end
``` ```
The dotted line indicates that the new reactive scope isn't actually connected `show()` will implicitly create an effect depending on `joined`, which can be
to the `show` on the graph, it is only managed internally through code. seen as `show` on the graph. This effect manages, and can create or destroy
a separate reactive scope seen as `show() scope` on the graph. The dotted line
indicates that it isn't actually connected, only indirectly managed through
code.
## switch() ## switch()
@ -135,11 +138,11 @@ The switch can map any value to any component.
```lua ```lua
type ActiveMenu = "none" | "inventory" | "shop" | "settings" type ActiveMenu = "none" | "inventory" | "shop" | "settings"
local menu = source "none" local menu = source "inventory"
switch(menu) { switch(menu) {
inventory = InventoryMenu, inventory = InventoryMenu,
shop = ShopMenu. shop = ShopMenu,
settings = SettingsMenu settings = SettingsMenu
} }
``` ```
@ -159,25 +162,25 @@ The reactive graph for the above example:
} }
}}%% }}%%
flowchart graph
subgraph root["mount() scope"] subgraph root["mount() scope"]
direction LR direction LR
joined --> show -.- subroot menu --> switch -.- subroot
subgraph subroot["switch() scope"] subgraph subroot["switch() scope"]
direction LR direction LR
Button Menu
end end
end end
``` ```
## indexes() ## indexes()
Often, you will have a table of values that will be displayed in a similar Often, you will have a table of values with each value displayed in a similar
manner. Rather than manually looping over each value to generate a corresponding manner. Rather than manually looping over each value to generate a corresponding
UI element, `indexes()` allows you to create an instance for each table index, UI element, `indexes()` allows you to create elements for each table index, to
to display the value at that index. display the value at that index.
```lua ```lua
local todoList = source { local todoList = source {
@ -204,13 +207,22 @@ end
TodoList { list = todoList } TodoList { list = todoList }
``` ```
For each unique index in the passed table, the transform function will be called For each index in the given source table, the given function will be called
with 1. a source containing the value of the index, 2. the index itself. 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 When the value at an index is changed, the function is not reran. Instead, the
given source for that index is updated. given source for that index is updated.
An element is only destroyed if the value of an index is set to `nil`. 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 reactive scope destroyed to clean up that
element.
`indexes()` is said to *map* each table index to a new UI element that can
update to display the current value at that index.
The reactive graph for the above example: The reactive graph for the above example:
@ -227,7 +239,7 @@ The reactive graph for the above example:
} }
}}%% }}%%
flowchart graph
subgraph root ["mount() scope"] subgraph root ["mount() scope"]
direction LR direction LR
@ -245,5 +257,20 @@ subgraph root ["mount() scope"]
end end
``` ```
One thing to note regarding table sources, is that when you edit a table in a
source, you must set that table again to actually update the source.
```lua
local src = source { 1, 2 }
local data = src()
table.insert(data, 3) -- no effects will run
src(data) -- effects will run
```
Together, these control flow functions cover the majority of cases where you Together, these control flow functions cover the majority of cases where you
need to dynamically create and destroy parts of your UI. need to dynamically create and destroy parts of your UI.
If you need to do something that these control flow functions cannot, you can
always use `mount()` within an effect to dynamically create and destroy
components on your own terms. Just remember to use `cleanup()` to unmount when
the effect reruns.

View file

@ -77,7 +77,7 @@ be parented.
```lua ```lua
type Children = { type Children = {
-- allows us to also optionally pass a source that returns an array of children instead -- also can optionally pass a source that returns an array of children too
Children = Array<Instance> | () -> Array<Instance> Children = Array<Instance> | () -> Array<Instance>
} }

View file

@ -1,8 +1,7 @@
# Actions # Actions
Actions in Vide are special callbacks that you can pass along with properties, Actions in Vide are special callbacks that you can pass along with properties,
which will be called when those properties are being processed with the instance to run some code on an instance receiving them.
being assigned to, allowing you to run custom code.
```lua ```lua
local action = vide.action local action = vide.action
@ -20,24 +19,22 @@ create "TextLabel" {
-- will print "test" -- will print "test"
``` ```
Actions can be wrapped with functions to re-use specific behaviors. Below is Actions can be wrapped with functions for reuse. Below is an example of an
an example of an action used to listen for property changes: action used to listen for property changes:
```lua ```lua
local action = vide.action local action = vide.action
local cleanup = vide.cleanup local cleanup = vide.cleanup
local function changed(property: string, callback: (new) -> ()) local function changed(prop: string, callback: (new) -> ())
return action(function(instance) return action(function(instance)
local con = instance:GetPropertyChangedSignal(property):Connect(function() local connection = instance:GetPropertyChangedSignal(prop):Connect(function()
callback(instance[property]) callback(instance[property])
end) end)
-- remember to clean up the connection when the reactive scope the action -- remember to clean up the connection when the reactive scope the action
-- is ran in is destroyed, so the instance can be garbage collected -- is ran in is destroyed, so the instance can be garbage collected
cleanup(function() cleanup(connection)
con:Disconnect()
end)
end) end)
end end

View file

@ -5,9 +5,13 @@ 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 will add extra safety checks and emit better error traces, particularly when
errors occur in property bindings. errors occur in property bindings.
Strict mode is automatically enabled when Vide is required in O0 or O1
optimization (default studio level). You can `vide.strict = false` if you do not
want this.
Strict mode will run derived sources and effects twice each time they update. Strict mode will run derived sources and effects twice each time they update.
This is to help identify improper cleanup of side-effects and ensure that pure This is to help ensure that derived source computations are pure, and that any
computations are actually pure. cleanups made in derived sources or effects are done correctly.
```lua ```lua
local source = vide.source local source = vide.source

View file

@ -45,19 +45,10 @@ 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 number key to set a child. Events can be connected to by assigning a function
to a string key. to a string key.
You can also use a shorthand to create datatypes instead of explicitly typing ::: warning
out the class name and constructor. The table will be unpacked into the `.new()`
constructor of the property's type.
```lua
create "Frame" {
AnchorPoint = { 0.5, 1 },
UDim2 = { 0.5, 0, 0.5, 0 }
}
```
When creating an instance with no properties, it is important to not forget to When creating an instance with no properties, it is important to not forget to
actually call the constructor: `create "Frame" {}` and not `create "Frame"`. actually call the constructor: `create "Frame" {}` and not `create "Frame"`.
To be clear, `create "Frame"` returns a *function* which is a constructor for 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 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. to parent a function instead of an instance which is not correct.
:::

View file

@ -1,9 +1,11 @@
# Components # Components
Components are custom-made reusable pieces of UI made from other pieces of UI. A component is a function that creates and returns a piece of UI.
By using components you can make your application more modular and better This is a way to separate your app into small chunks that you can reuse and put
organized. together.
::: code-group
```lua [Button.luau] ```lua [Button.luau]
local create = vide.create local create = vide.create
@ -15,11 +17,14 @@ local function Button(props: {
}) })
return create "TextButton" { return create "TextButton" {
BackgroundColor3 = Color3.fromRGB(50, 50, 50), BackgroundColor3 = Color3.fromRGB(50, 50, 50),
TextColor3 = Color3.fromRGB(255, 255, 255),
Size = UDim2.fromOffset(200, 150), Size = UDim2.fromOffset(200, 150),
Position = props.Position, Position = props.Position,
Text = props.Text, Text = props.Text,
Activated = props.Activated Activated = props.Activated,
create "UICorner" {}
} }
end end
@ -36,10 +41,17 @@ local function App()
return create "ScreenGui" { return create "ScreenGui" {
Button { Button {
Position = UDim2.fromOffset(200, 200), Position = UDim2.fromOffset(200, 200),
Text = "click me!", Text = "back",
Activated = function() Activated = function()
print "clicked" print "go to previous page"
end
},
Button {
Position = UDim2.fromOffset(400, 200),
Text = "next",
Activated = function()
print "go to next page"
end end
} }
} }
@ -48,8 +60,9 @@ end
mount(App, game.StarterGui) mount(App, game.StarterGui)
``` ```
Above is a simple example of a button component with a set color and size, :::
being reused across files.
Above is a simple example of a button component being used across files.
A single parameter `props` is used to pass properties to the component. A single parameter `props` is used to pass properties to the component.
@ -57,8 +70,8 @@ Components allow you to *encapsulate* behavior. You can only modify the
component in ways that you allow in the component, through the `props` parameter. component in ways that you allow in the component, through the `props` parameter.
To create a new button all you must do is call the `Button` function, passing in 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 values. This saves having to create and set every property each time. Also, when
time. Also, when updating the button component in future, any changes to the updating the button component in future, any changes to the button file will be
button file will be seen anywhere the button is used in your app. seen anywhere the button is used in your app.
This can be extended to much more complicated UI. This can be extended to much more complicated UI.

View file

@ -1,10 +1,9 @@
# Sources # Sources
*Sources* in Vide are special objects that store a single value. They are the Sources are special objects that store a single value. They are the core of
core of reactivity in Vide. Each source represents a source of data, and they Vide's reactivity. They are called sources because they act as sources of data.
can be composed and derived to create new sources of data.
A source in Vide can be created using `source()`. A source can be created using `source()`.
```lua ```lua
local source = vide.source local source = vide.source

View file

@ -21,12 +21,9 @@ count(1)
-- "count: 1" printed -- "count: 1" printed
``` ```
The callback given to `effect()` is ran in a *reactive scope*. Any source read The callback given to `effect()` is initially ran immediately in a
from inside a reactive scope will be tracked, so that if any of those sources *reactive scope*. Any source read from inside a reactive scope will be tracked,
update, the effect will be reran too. so that if any of those sources update, the effect will be reran too.
The callback is first ran immediately inside the `effect()` call to initially
track sources used.
Effects also work with derived sources, it doesn't matter how deeply nested Effects also work with derived sources, it doesn't matter how deeply nested
inside a function a source is. inside a function a source is.
@ -50,23 +47,5 @@ count(2)
-- "doubled count: 4" printed -- "doubled count: 4" printed
``` ```
The reactive graph for the above example: If a source is updated with the same value it already had, it will not rerun
effects depending on it.
```mermaid
%%{init: {
"theme": "base",
"themeVariables": {
"primaryColor": "#1B1B1F",
"primaryTextColor": "#fff",
"primaryBorderColor": "#1B1B1F",
"lineColor": "#79B8FF",
"tertiaryColor": "#161618",
"tertiaryBorderColor": "#fff"
}
}}%%
flowchart LR
count --> effect
```

View file

@ -1,7 +1,7 @@
# Root Reactive Scopes # Root Reactive Scopes
Any reactive scopes created, such as one from `effect()`, must be done so within Any reactive scopes created, such as by `effect()`, must be done so within a
a "root" reactive scope. This is the main purpose of `mount()`, which you use "root" reactive scope. This is the main purpose of `mount()`, which you use
once at the top level to create your app as shown in the first introduction. once at the top level to create your app as shown in the first introduction.
This is so that when the app is unmounted, it can clean up any reactive scopes This is so that when the app is unmounted, it can clean up any reactive scopes
@ -25,7 +25,18 @@ vide.mount(App) -- works!
App() -- will error since effect() was not called within a reactive scope App() -- will error since effect() was not called within a reactive scope
``` ```
The reactive graph for the above example: Mounting returns a function that when called will destroy any reactive scopes
created during the `mount()` call.
```lua
local unmount = mount(App)
unmount()
```
Vide's reactivity can be represented graphically, as a *reactive graph*.
The reactive graph for the above example looks like so:
```mermaid ```mermaid
%%{init: { %%{init: {
@ -40,10 +51,18 @@ The reactive graph for the above example:
} }
}}%% }}%%
flowchart graph
subgraph root subgraph root["mount"]
direction LR direction LR
count --> effect count --> effect
end end
``` ```
When the `mount` scope is destroyed, the `effect` scope will also be destroyed
since it was created within it.
You don't need to worry about ensuring all your effects are created within a
root scope, since you should be creating all your UI and corresponding effects
within a top-level `mount()` call that puts all your UI together. So it is safe
to assume that any effect you create will be created under this top level scope.

View file

@ -5,6 +5,8 @@ A stateful component is a component that stores and displays some data.
Stateful components in Vide are created using sources and effects - sources to Stateful components in Vide are created using sources and effects - sources to
store the data, and effects to display the data. store the data, and effects to display the data.
## Internal State
```lua ```lua
local create = vide.create local create = vide.create
local source = vide.source local source = vide.source
@ -30,12 +32,14 @@ end
Above is an example of a counter component, that when clicked, will increment 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. its internal count, and automatically update its text to reflect that count.
Making a property update based on a source is also called *property Making a property update based on a source is also referred to as *property
binding*. binding*.
Each instance of `Counter()` will maintain its own independent count, since the Each instance of `Counter()` will maintain its own independent count, since the
count source is created inside the scope of the component. count source is created inside the scope of the component.
## External State
External sources can also be passed into components for them to use. External sources can also be passed into components for them to use.
```lua ```lua
@ -65,5 +69,5 @@ count(1) -- the Counter component will update to display this count
``` ```
Sources can be created internally or passed in from externally, there are no 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 restrictions on how they are used as long as the effect using it is created
reactive scope so that it can be tracked. within a reactive scope so that it can be cleaned up later.

View file

@ -20,6 +20,7 @@ local function Counter()
count(count() + 1) count(count() + 1)
end end
} }
end
``` ```
This example is equivalent to the example seen on the previous page. This example is equivalent to the example seen on the previous page.
@ -32,15 +33,16 @@ 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. 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 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 sources map to UI, and
reactive system will automatically update any properties depending on sources Vide's reactive system will automatically update any properties depending on
that are updated. those sources that were updated.
## Children Binding ## Children Binding
Children can also be set in a similar manner. Sources bound to properties can Children can also be set in a similar manner. A source passed as a child (passed
return an instance or an array of instances. Vide will automatically unparent with a number key instead of string key) can return an instance or an array of
removed instances and parent new instances. instances. Vide will automatically unparent removed instances and parent new
instances when that source's stored instances change.
```lua ```lua
local items = source { local items = source {

View file

@ -60,17 +60,15 @@ effect(function()
end) end)
effect(function() effect(function()
text() -- does not print text() -- does not print, returns cached value
end) 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,
improper usage could accidently create infinite loops in the reactive graph.
Always favour deriving when you need one source to update based on another.
`derive()` must also be used within a root reactive scope, just like `effect()`. `derive()` must also be used within a root reactive 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.
The reactive graph for the above example: The reactive graph for the above example:
```mermaid ```mermaid
@ -86,10 +84,15 @@ The reactive graph for the above example:
} }
}}%% }}%%
flowchart graph
subgraph root subgraph root
direction LR direction LR
count --> text --> effect1 & effect2 count --> text --> effect1 & effect2
end 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,
improper usage could accidently create infinite loops in the reactive graph.
Always favour deriving when you need one source to update based on another.