diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 51b477a..5c32428 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -49,10 +49,9 @@ export default withMermaid({ { text: "Derived Sources", link: "/tut/crash-course/9-derived-source" }, { text: "Cleanup", link: "/tut/crash-course/10-cleanup" }, { text: "Control Flow", link: "/tut/crash-course/11-control-flow" }, - { text: "Property Nesting", link: "/tut/crash-course/12-property-nesting" }, - { text: "Actions", link: "/tut/crash-course/13-actions" }, - { text: "Strict Mode", link: "/tut/crash-course/14-strict-mode" }, - { text: "Concepts Summary", link: "/tut/crash-course/15-concepts" } + { text: "Actions", link: "/tut/crash-course/12-actions" }, + { text: "Strict Mode", link: "/tut/crash-course/13-strict-mode" }, + { text: "Concepts Summary", link: "/tut/crash-course/14-concepts" } ] }, { diff --git a/docs/tut/crash-course/1-introduction.md b/docs/tut/crash-course/1-introduction.md index 261ce83..983f51b 100644 --- a/docs/tut/crash-course/1-introduction.md +++ b/docs/tut/crash-course/1-introduction.md @@ -4,22 +4,12 @@ This is a tutorial that introduces the concepts and usage of Vide. Vide is heavily inspired by [Solid](https://www.solidjs.com/). -This tutorial assumes familiarity with Luau and Roblox UI. - ## Why Vide? -Creating UI is complicated, slow, and tedious. - -Vide tries to simplify and speed up this process by providing a declarative and -reactive of style programming, which lets you focus more on designing the UI -itself and not having to manually update or reparent UI instances. +Vide provides a reactive and declarative API to simplify managing UI. Some of the main focuses behind Vide's design choices: -- Minimal syntax. +- Minimal syntax - Complete typechecking -- Independence from instances. - -As with most declarative libraries, there is an initial learning curve to -understand the concepts and usage. This tutorial tries to comprehensively -cover these concepts and usage, more so than you need just to use it. +- Independence from instances diff --git a/docs/tut/crash-course/10-cleanup.md b/docs/tut/crash-course/10-cleanup.md index 474f1f2..a863093 100644 --- a/docs/tut/crash-course/10-cleanup.md +++ b/docs/tut/crash-course/10-cleanup.md @@ -2,48 +2,33 @@ 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 queue a cleanup callback for the next time a reactive scope is rerun -or destroyed, or when a stable scope is destroyed. +is used to queue a callback for the next time a reactive scope is rerun or +destroyed, or when a stable scope is destroyed. ```lua -local mount = vide.mount +local root = vide.root local source = vide.source -local cleanup = vide.cleanup +local effect = vide.effect -local function Timer() - local count = source(0) +local count = source(0) - local con = game:GetService("RunService").Heartbeat:Connect(function(dt) - count(count() + dt) + +local destroy = root(function(destroy) + effect(function() + local x = count() + cleanup(function() print(x) end) end) - cleanup(function() - con:Disconnect() - end) + cleanup(function() print "root destroyed" end) - return create "TextButton" { - Position = UDim2.fromOffset(300, 300), - Size = UDim2.fromOffset(200, 50), - - Text = function() - return "seconds: " .. math.floor(count()) - end, - } -end - -local instance, destroy = root(function(destroy) - local instance = Timer() - return instance, destroy + return destroy end) -wait(5) - -destroy() -- all queued cleanups are ran, heartbeat connection disconnected +count(1) -- prints "0" +count(2) -- prints "1" +destroy() -- prints "2" and "root destroyed" ``` -In the above example, this allows us to disconnect the heartbeat connection -when the scope responsible for creating the timer component is destroyed. - ::: tip Roblox instances do not need to be explicitly destroyed for their memory to be freed, they only need to be parented to `nil`. So there is no diff --git a/docs/tut/crash-course/11-control-flow.md b/docs/tut/crash-course/11-control-flow.md index 246d65b..b206915 100644 --- a/docs/tut/crash-course/11-control-flow.md +++ b/docs/tut/crash-course/11-control-flow.md @@ -5,119 +5,26 @@ resulting from source updates. Vide provides functions to help you do this, known as *control flow* functions. These functions return new sources, which hold the instances to be displayed. - -Control flow functions run their components in a new stable scope, which can -be destroyed independently of the stable scope that called the control flow -function. This means parts of your app can be independently created and -destroyed. - -## switch() - -`switch()` condtionally displays one instance at a time. It uses a table to map -a source value to a component. - -```lua -local source = vide.source -local switch = vide.switch - -local function Button(props: { - Text: string, - Activated: () -> () -}) - local hovered = source(false) - - return create "TextButton" { - Text = props.Text, - Activated = props.Activated, - - TextColor3 = function() - return hovered() and Color3.new(1, 1, 1) or Color3.new(.7, .7, .7) - end, - - MouseEnter = function() hovered(true) end, - MouseLeave = function() hovered(false) end - } -end - -local function JoinMenu() - local joined = source(false) - - local function JoinButton() - return Button { - Text = "Join", - Activated = function() joined(true) end - } - end - - local function LeaveButton() - return Button { - Text = "Leave" - Activated = function() joined(false) end - } - end - - return create "Frame" { - switch(joined) { - [true] = LeaveButton, - [false] = JoinButton - } - } -end -``` - -The reactive graph for the above example: - -```mermaid -%%{init: { - "theme": "base", - "themeVariables": { - "primaryColor": "#1B1B1F", - "primaryTextColor": "#fff", - "primaryBorderColor": "#1B1B1F", - "lineColor": "#79B8FF", - "tertiaryColor": "#161618", - "tertiaryBorderColor": "#1C1C1F" - } -}}%% - -graph - -subgraph root["root scope"] - direction LR - joined --> switch -.- subroot - - subgraph subroot["switch scope"] - direction LR - effect["TextColor3 effect"] - end -end -``` - -A `switch()` call creates a new effect and a new stable scope as seen in the -above graph. Whenever `menu` updates, it causes the `switch` effect to run, -which will destroy and recreate the switch scope with the new component. - -This will also destroy the internal effect that the button uses to highlight -itself when it is hovered, each time the switch is rerun. +The new sources can be used in `create()` to update the children of a container +instance. ## indexes() -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 -UI element, `indexes()` allows you to create elements each corresponding to a -table index, to display the value at that index. +`indexes()` *maps* each table index to a new UI element that can +update to display the current value at that index. Each table index is given a +single corresponding UI element. ```lua -local todoList = source { +local list = source { "finish the crash course", - "star vide's GitHub" + "star Vide's GitHub" } local function TodoList(props: { list: () -> Array }) return create "Frame" { create "UIListLayout" {}, - indexes(todoList, function(todo, i) + indexes(list, function(todo, i) return create "TextLabel" { Text = function() return i .. ": " .. todo() @@ -129,13 +36,13 @@ local function TodoList(props: { list: () -> Array }) } end -TodoList { list = todoList } +TodoList { list = list } ``` -For each index in the given source table, the given function will be called -with: +For each index in the given source table, the given function to `indexes()` will +be run in a new stable scope with: -1. a source containing the value of the index +1. a source containing the value at the index 2. the index itself When the value at an index is changed, the function is not reran. Instead, the @@ -143,12 +50,9 @@ given source for that index is updated. 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. +value), will have its corresponding stable scope destroyed. + -`indexes()` is said to *map* each table index to a new UI element that can -update to display the current value at that index. Each table index is given a -single corresponding UI element. The reactive graph for the above example: @@ -183,8 +87,8 @@ subgraph root ["root scope"] 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. +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 } @@ -192,11 +96,3 @@ 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 -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. diff --git a/docs/tut/crash-course/13-actions.md b/docs/tut/crash-course/12-actions.md similarity index 100% rename from docs/tut/crash-course/13-actions.md rename to docs/tut/crash-course/12-actions.md diff --git a/docs/tut/crash-course/12-property-nesting.md b/docs/tut/crash-course/12-property-nesting.md deleted file mode 100644 index d8c44ce..0000000 --- a/docs/tut/crash-course/12-property-nesting.md +++ /dev/null @@ -1,120 +0,0 @@ -# Nested Properties - -Often when creating components from existing components, you can find yourself -repetitively passing through properties such as size or position. - -Example below: - -```lua -function Background(props: { - Color: Color3, - AnchorPoint: UDim2, - Position: UDim2, - Size: UDim2 -}) - return create "Frame" { - Color = props.Color - AnchorPoint = props.AnchorPoint, - Position = props.Position, - Size = props.Size - } -end - -function Menu(props: { - Color = props.Color - AnchorPoint: UDim2, - Position: UDim2, - Size: UDim2 -}) - return Background { - Color = props.Color, - AnchorPoint = props.AnchorPoint, - Position = props.Position, - Size = props.Size - } -end -``` - -One way this can be avoided is by using *property nesting*. In Vide, passing a -table value inside `props` has special semantics. Any key with a table value is -not assigned like a property, instead the table is iterated and processed just -like the outer table is. Any properties in the nested table will be assigned -to the instance just the same. - -Below is an example of how you can use this to pass groups of similar properties -together such as position and size, while also using typechecking. - -```lua -type Layout = { - Layout = { - Position: UDim2?, - Size: UDim2?, - AnchorPoint: Vector2? - } -} - -function Background(props: Layout & { Color: Color3 }) - return create "Frame" { - Color = props.Color, - props.Layout - } -end - -function Menu(props: Layout & { Color: Color3 }) - return Background { - Color = props.Color, - Layout = props.Layout - } -end -``` - -Here we created a nested group with the key `Layout` that can accept -layout-related properties. Any name could be chosen for the key. -This allows us to write much more concise syntax that is also typecheckable. - -In another example we use a key named `Children` to pass arrays of instances to -be parented. - -```lua -type Children = { - -- also can optionally pass a source that returns an array of children too - Children = Array | () -> Array -} - -local function List(props: Children & Layout) - return create "Frame" { - props.Children, - props.Layout, - create "UIListLayout" {} - } -end - -List { - Layout = { - Position = UDim2.new() - }, - - Children = { - create "TextLabel" { Text = "1" }, - create "TextLabel" { Text = "2" } - } -} -``` - -Deeper nested properties are guaranteed to be set after shallower nested -properties, this can be used to create overridable default properties. - -```lua -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), - - create "UIListLayout" {} - } -end -``` diff --git a/docs/tut/crash-course/14-strict-mode.md b/docs/tut/crash-course/13-strict-mode.md similarity index 100% rename from docs/tut/crash-course/14-strict-mode.md rename to docs/tut/crash-course/13-strict-mode.md diff --git a/docs/tut/crash-course/15-concepts.md b/docs/tut/crash-course/14-concepts.md similarity index 100% rename from docs/tut/crash-course/15-concepts.md rename to docs/tut/crash-course/14-concepts.md diff --git a/docs/tut/crash-course/2-creation.md b/docs/tut/crash-course/2-creation.md index 3cc4fd5..6dace52 100644 --- a/docs/tut/crash-course/2-creation.md +++ b/docs/tut/crash-course/2-creation.md @@ -2,11 +2,8 @@ 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 which is recommended to use for brevity. +Parentheses `()` can be omitted when calling functions with string or +table literals which is recommended for brevity. ```lua local create = vide.create diff --git a/docs/tut/crash-course/3-components.md b/docs/tut/crash-course/3-components.md index e3cc730..8e5890f 100644 --- a/docs/tut/crash-course/3-components.md +++ b/docs/tut/crash-course/3-components.md @@ -34,12 +34,12 @@ end return Button ``` -```lua [App.luau] +```lua [Menu.luau] local create = vide.create local Button = require(Button) -local function App() +local function Menu() return create "ScreenGui" { Button { Position = UDim2.fromOffset(200, 200), diff --git a/docs/tut/crash-course/4-source.md b/docs/tut/crash-course/4-source.md index c356b6b..08da45c 100644 --- a/docs/tut/crash-course/4-source.md +++ b/docs/tut/crash-course/4-source.md @@ -1,7 +1,7 @@ # Sources -Sources are special objects that store a single value. They are the core of -Vide's reactivity. They are called sources because they act as sources of data. +Sources are special objects that store a single value and are the core of +Vide's reactivity. A source can be created using `source()`. @@ -20,8 +20,7 @@ by calling it with no arguments. count(count() + 1) -- increment count by 1 ``` -Sources can be *derived* by wrapping them in functions. A wrapped source -effectively becomes a new source. +Sources can be *derived* by wrapping them in functions. ```lua local count = source(0) @@ -35,7 +34,5 @@ count(1) print(text()) -- "count: 1" ``` -Sources on their own aren't very special, the above can be achieved with plain -variables. The real use for sources become apparent when used in combination -with *effects*. Similar to a signal and connection, a source and effect allows -you to do things like automatically updating UI when a source is updated. +While the above can be achieved with plain variables, the use for sources will +be obvious in the next part. diff --git a/docs/tut/crash-course/5-effect.md b/docs/tut/crash-course/5-effect.md index 275a2e6..5e6043c 100644 --- a/docs/tut/crash-course/5-effect.md +++ b/docs/tut/crash-course/5-effect.md @@ -1,8 +1,7 @@ # Effects Effects are functions that are ran in response to source updates. They are -called effects because they cause *side-effects* when reacting to source -updates. +A source and effect is analogous to a signal and connection. Effects are created using `effect()`. @@ -21,11 +20,10 @@ count(1) -- "count: 1" printed ``` -The callback given to `effect()` is ran immediately in a *reactive scope*. Any -source read from inside a reactive scope will be tracked, so when any of those -sources update, the effect will be reran too. +Any source read inside an effect is tracked and will rerun the effect when +that source is updated. -Reactive scopes also track derived sources, it doesn't matter how deeply nested +Derived sources are also tracked, it doesn't matter how deeply nested inside a function a source is. ```lua diff --git a/docs/tut/crash-course/6-scope.md b/docs/tut/crash-course/6-scope.md index 6148364..fa6ea8f 100644 --- a/docs/tut/crash-course/6-scope.md +++ b/docs/tut/crash-course/6-scope.md @@ -1,25 +1,29 @@ # Scopes -Vide operates on the concept of scopes. Vide scopes come in two flavors: -stable and reactive. +Just like how a signal's connection may need to be disconnected, a source's +effect also may need to be disconnected. -The three main rules for scopes are: +But the disconnecting of many signals and connections is tedious and verbose. +Vide instead operates on the concept of scopes which provides a much cleaner +API, given that you follow a few rules. -- Stable scopes never rerun. -- Reactive scopes will rerun on source updates. -- A reactive scope cannot be created within another reactive scope. +Scopes come in two flavors; stable and reactive. -Reactive scopes cannot be created on their own - they must be created within -a stable scope so that it can be tracked and later destroyed when it is -no longer needed. +- All scopes must be created within another scope with the exception of `root()` +- Stable scopes never rerun +- Reactive scopes can rerun +- A reactive scope cannot be created within another reactive scope -This is the purpose of `root()`, which creates an initial stable scope, which -all other reactive scopes, such as ones created by `effect()`, can stem from. +`effect()` creates a reactive scope. +`root()` creates a stable scope. -When this root reactive scope is destroyed, it will destroy any effects created -within it, ensuring everything is cleaned up properly. +Whenever a scope is destroyed, any scope created within that scope is also +destroyed, and so on. This is why all scopes must be created within another +scope, except `root()` which is used to create the initial scope that you can +manually destroy. ```lua +local root = vide.root local source = vide.source local effect = vide.effect @@ -33,9 +37,9 @@ local function setup() return count end -setup() -- will error since effect() was not called within a stable scope +setup() -- will error since effect() tries to create a reactive scope outside of a stable scope -local count = vide.root(setup) -- runs +local count = root(setup) -- ok since effect() was called within a stable scope count(1) -- prints "1" ``` @@ -87,7 +91,7 @@ subgraph root end ``` -When the root reactive scope created by `root()` is destroyed, the `effect` +When the stable `root()` is destroyed, the reactive `effect()` scope will also be destroyed since it was created within it. This is important because you may have an effect that updates the property of a @@ -96,7 +100,6 @@ memory. The effect being destroyed will remove this reference, allowing the instance to be garbage collected. You don't need to worry about ensuring all your effects are created within a -root reactive scope, since you should be creating all your UI and corresponding -effects within a top-level `root()` 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. Vide will prevent you from accidently doing otherwise anyways. +stable scope, since you should be creating all your UI and effects within a +single top-level `root()` call that puts all your UI together, making it safe to +assume any effect created will be created under this stable scope. diff --git a/docs/tut/crash-course/7-stateful-component.md b/docs/tut/crash-course/7-stateful-component.md index 427d0ee..493f9bc 100644 --- a/docs/tut/crash-course/7-stateful-component.md +++ b/docs/tut/crash-course/7-stateful-component.md @@ -1,7 +1,5 @@ # Stateful Components -A stateful component is a component that stores some data internally. - Stateful components in Vide are created using sources and effects - sources to store the data, and effects to display the data. diff --git a/docs/tut/crash-course/8-implicit-effect.md b/docs/tut/crash-course/8-implicit-effect.md index 60d7823..bbd92d4 100644 --- a/docs/tut/crash-course/8-implicit-effect.md +++ b/docs/tut/crash-course/8-implicit-effect.md @@ -1,7 +1,7 @@ # Implicit Effects -Explicitly creating effects to update properties can be tedious. Vide provides a -way to *implicitly* create an effect to update properties. +Explicitly creating effects to update properties is tedious. You can +*implicitly* create an effect to update properties instead. ```lua local create = vide.create @@ -25,18 +25,14 @@ 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 an effect to update that property anytime a -source used within 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 used. +function will implicitly create an effect to update that property. ## Children Children can also be set in a similar manner. A source passed as a child (passed with a number key instead of string key) can return an instance or an array of -instances. Vide will automatically unparent removed instances and parent new -instances when that source's stored instances change. +instances. An effect is automatically created to unparent removed instances and +parent new instances on source update. ```lua local items = source { @@ -57,5 +53,5 @@ items { create "TextLabel" { Text = "C" } } --- this will automatically unparent the text label "A", and parent the labels "B" and "C". +-- this will automatically unparent the text label "A", and parent the labels "B" and "C" ``` diff --git a/docs/tut/crash-course/9-derived-source.md b/docs/tut/crash-course/9-derived-source.md index c1e3669..ffcdaa9 100644 --- a/docs/tut/crash-course/9-derived-source.md +++ b/docs/tut/crash-course/9-derived-source.md @@ -36,14 +36,13 @@ source(1) -- prints "ran" x2 ``` To avoid this, you can use `derive()` to derive a new source instead. This will -run a function in a new reactive scope only when a dependent source has updated. -Reading this derived source multiple times will just return a cached result from -when it last updated. +run a function in a reactive scope only when a source used inside updated. +Reading this derived source multiple times will just return a cached result. ```lua local source = vide.source -local derive = vide.derive local effect = vide.effect +local derive = vide.derive local count = source(0) @@ -87,6 +86,7 @@ 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. +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 +source.