From bb08e2e97a9e4fddd78d1259d041023393c25596 Mon Sep 17 00:00:00 2001 From: Aaron Smith <83140718+centau@users.noreply.github.com> Date: Tue, 19 Sep 2023 14:52:28 +0100 Subject: [PATCH] Update crash course --- docs/.vitepress/config.ts | 16 ++-- docs/tut/crash-course/1-introduction.md | 5 -- ...erty-nesting.md => 10-property-nesting.md} | 5 +- .../{10-actions.md => 11-actions.md} | 17 +++- docs/tut/crash-course/12-strict-mode.md | 9 ++ docs/tut/crash-course/2-creation.md | 7 +- docs/tut/crash-course/3-components.md | 12 ++- docs/tut/crash-course/4-source.md | 50 +++++------ docs/tut/crash-course/5-effect.md | 86 +++++++++++++------ docs/tut/crash-course/6-derived-source.md | 76 ---------------- docs/tut/crash-course/6-stateful-component.md | 66 ++++++++++++++ docs/tut/crash-course/7-property-binding.md | 64 ++++++++++++++ .../{7-cleanup.md => 8-cleanup.md} | 14 ++- .../{8-control-flow.md => 9-control-flow.md} | 4 + todo.md | 1 + 15 files changed, 272 insertions(+), 160 deletions(-) rename docs/tut/crash-course/{9-property-nesting.md => 10-property-nesting.md} (94%) rename docs/tut/crash-course/{10-actions.md => 11-actions.md} (66%) create mode 100644 docs/tut/crash-course/12-strict-mode.md delete mode 100644 docs/tut/crash-course/6-derived-source.md create mode 100644 docs/tut/crash-course/6-stateful-component.md create mode 100644 docs/tut/crash-course/7-property-binding.md rename docs/tut/crash-course/{7-cleanup.md => 8-cleanup.md} (63%) rename docs/tut/crash-course/{8-control-flow.md => 9-control-flow.md} (94%) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 519d4d6..b889dc8 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -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" }, ] }, { diff --git a/docs/tut/crash-course/1-introduction.md b/docs/tut/crash-course/1-introduction.md index 7b00dac..371950a 100644 --- a/docs/tut/crash-course/1-introduction.md +++ b/docs/tut/crash-course/1-introduction.md @@ -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" } diff --git a/docs/tut/crash-course/9-property-nesting.md b/docs/tut/crash-course/10-property-nesting.md similarity index 94% rename from docs/tut/crash-course/9-property-nesting.md rename to docs/tut/crash-course/10-property-nesting.md index 1a61425..2aabb11 100644 --- a/docs/tut/crash-course/9-property-nesting.md +++ b/docs/tut/crash-course/10-property-nesting.md @@ -77,7 +77,8 @@ be parented. ```lua type Children = { - Children = Array + -- allows us to also optionally pass a source that returns an array of children instead + Children = Array | () -> Array } 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), diff --git a/docs/tut/crash-course/10-actions.md b/docs/tut/crash-course/11-actions.md similarity index 66% rename from docs/tut/crash-course/10-actions.md rename to docs/tut/crash-course/11-actions.md index 68fb06e..79ca7c7 100644 --- a/docs/tut/crash-course/10-actions.md +++ b/docs/tut/crash-course/11-actions.md @@ -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 diff --git a/docs/tut/crash-course/12-strict-mode.md b/docs/tut/crash-course/12-strict-mode.md new file mode 100644 index 0000000..c66af04 --- /dev/null +++ b/docs/tut/crash-course/12-strict-mode.md @@ -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). diff --git a/docs/tut/crash-course/2-creation.md b/docs/tut/crash-course/2-creation.md index deb1683..68e3007 100644 --- a/docs/tut/crash-course/2-creation.md +++ b/docs/tut/crash-course/2-creation.md @@ -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. diff --git a/docs/tut/crash-course/3-components.md b/docs/tut/crash-course/3-components.md index 712ab9a..3050cf8 100644 --- a/docs/tut/crash-course/3-components.md +++ b/docs/tut/crash-course/3-components.md @@ -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. diff --git a/docs/tut/crash-course/4-source.md b/docs/tut/crash-course/4-source.md index cf7ceb2..1583c59 100644 --- a/docs/tut/crash-course/4-source.md +++ b/docs/tut/crash-course/4-source.md @@ -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. diff --git a/docs/tut/crash-course/5-effect.md b/docs/tut/crash-course/5-effect.md index 67c1f58..3891652 100644 --- a/docs/tut/crash-course/5-effect.md +++ b/docs/tut/crash-course/5-effect.md @@ -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. diff --git a/docs/tut/crash-course/6-derived-source.md b/docs/tut/crash-course/6-derived-source.md deleted file mode 100644 index 01fe1c2..0000000 --- a/docs/tut/crash-course/6-derived-source.md +++ /dev/null @@ -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. diff --git a/docs/tut/crash-course/6-stateful-component.md b/docs/tut/crash-course/6-stateful-component.md new file mode 100644 index 0000000..ba167e2 --- /dev/null +++ b/docs/tut/crash-course/6-stateful-component.md @@ -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. diff --git a/docs/tut/crash-course/7-property-binding.md b/docs/tut/crash-course/7-property-binding.md new file mode 100644 index 0000000..b3840cc --- /dev/null +++ b/docs/tut/crash-course/7-property-binding.md @@ -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". +``` diff --git a/docs/tut/crash-course/7-cleanup.md b/docs/tut/crash-course/8-cleanup.md similarity index 63% rename from docs/tut/crash-course/7-cleanup.md rename to docs/tut/crash-course/8-cleanup.md index 3e5525d..3a04463 100644 --- a/docs/tut/crash-course/7-cleanup.md +++ b/docs/tut/crash-course/8-cleanup.md @@ -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. diff --git a/docs/tut/crash-course/8-control-flow.md b/docs/tut/crash-course/9-control-flow.md similarity index 94% rename from docs/tut/crash-course/8-control-flow.md rename to docs/tut/crash-course/9-control-flow.md index f590145..fdde0ae 100644 --- a/docs/tut/crash-course/8-control-flow.md +++ b/docs/tut/crash-course/9-control-flow.md @@ -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. diff --git a/todo.md b/todo.md index 9fb48b6..933d2b5 100644 --- a/todo.md +++ b/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)`