From c796e48173dfc42d3cad1c0d13bb264190cf2621 Mon Sep 17 00:00:00 2001 From: aaron <83140718+centau@users.noreply.github.com> Date: Sat, 4 Jan 2025 00:40:51 +0000 Subject: [PATCH 1/6] Add source to `show()` callback --- CHANGELOG.md | 2 ++ src/show.luau | 34 +++++++++++++++++--------- test/tests.luau | 65 ++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 84 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef49bb2..fee931d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). effects to set children. - `spring()` returns a second value, a setter to set position, velocity and impulse. +- `show()` now receives a source to its callback returning the current value + of the condition. ### Changed diff --git a/src/show.luau b/src/show.luau index b3c3fef..e33393f 100644 --- a/src/show.luau +++ b/src/show.luau @@ -1,16 +1,28 @@ -local switch = require "./switch" +local derive = require "./derive" +local untrack = require "./untrack" -local function show(source: () -> any, component: () -> T, fallback: (() -> T)?): () -> T? - local function truthy() +local function show(source: () -> T?, component: (() -> T) -> U, fallback: (() -> U)?): () -> U? + local truthy = derive(function() return not not source() - end + end) - return switch(truthy) { - [true] = component, - [false] = fallback, - } + -- seemingly redundant derivation to extend the reactive graph so that + -- the propogation of the source's update is delayed, giving time for + -- the show() scope to be destroyed before potential effects registered + -- by the component can run when they should not run + -- todo: are there cases this method does not cover? + local derived = derive(function() + return source() + end) + + return derive(function() + return + if truthy() then untrack(function() return component(derived :: () -> T) end) + elseif fallback then untrack(fallback) + else nil + end) end -return show :: - ((source: () -> any, component: () -> T) -> () -> T?) & - ((source: () -> any, component: () -> T, fallback: () -> U) -> () -> (T | U)?) +return show :: ( + ( (source: () -> T?, component: (() -> T) -> U) -> () -> U? ) +) diff --git a/test/tests.luau b/test/tests.luau index d4c95ae..d79e966 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -1073,20 +1073,73 @@ TEST("show()", wrap_root(function() local show = vide.show local root = vide.root - do CASE "main" - -- uses switch() internally, more extensive testing of scoping not needed - local value = source("truey" :: unknown) + do CASE "show component" + local input = source(true) + local function one() return 1 end + + local output = show(input, one) + + CHECK(output() == 1) + input(false) + CHECK(output() == nil) + end + + do CASE "fallback component" + local input = source(true) local function one() return 1 end local function two() return 2 end - local output = show(value, one, two) + local output = show(input, one, two) CHECK(output() == 1) - value(nil) + input(false) CHECK(output() == 2) end - do CASE "alt" + do CASE "updating truth to truthy does not rerun" + local input = source(1) + local count = 0 + + local function component() + count += 1 + return 1 + end + + local output = show(input, component) + + CHECK(count == 1) + CHECK(output() == 1) + input(2) + CHECK(count == 1) + end + + do CASE "updating source passed to component" + local input = source(1 :: number?) + local count = 0 + + show(input :: () -> number?, function(value: () -> number) + vide.cleanup(function() print "destroyed" end) + effect(function() + local v = value() + + count += 1 + + CHECK(v == count) + if v ~= count then error(count) end + end) + + return true + end) + + input(2) + CHECK(count == 2) + input(3) + CHECK(count == 3) + input(nil) + CHECK(count == 3) + end + + do CASE "alt" -- todo: move test local visible = vide.source(true) local count = vide.source(0) From 46a20433566021da6f527d966324e557cce5283f Mon Sep 17 00:00:00 2001 From: aaron <83140718+centau@users.noreply.github.com> Date: Sat, 4 Jan 2025 00:45:55 +0000 Subject: [PATCH 2/6] Ignore `false` passed as a child --- CHANGELOG.md | 1 + src/apply.luau | 2 +- test/tests.luau | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fee931d..00f644f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). impulse. - `show()` now receives a source to its callback returning the current value of the condition. +- Ignore `false` passed as a child. ### Changed diff --git a/src/apply.luau b/src/apply.luau index 95645da..05ba98a 100644 --- a/src/apply.luau +++ b/src/apply.luau @@ -93,7 +93,7 @@ local function process_properties(properties: Map, instance: I else process_properties(value :: Map, instance, cache, depth + 1) end - else + elseif type(value) == "userdata" then (value :: Instance).Parent = instance -- parent child end end diff --git a/test/tests.luau b/test/tests.luau index d79e966..36a22b8 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -819,6 +819,20 @@ TEST("create()", wrap_root(function() CHECK(frame:FindFirstChild "G") end + do CASE "set false as child" + create "Frame" { + false + } + + create "Frame" { + function() return false end + } + + create "Frame" { + function() return { false } end + } + end + do CASE "binding properties to source" local name = source("Hi") local text = source("Bye") From a383c4ce69ddf6e59592e4e46163a29b45e104a2 Mon Sep 17 00:00:00 2001 From: aaron <83140718+centau@users.noreply.github.com> Date: Mon, 6 Jan 2025 23:56:49 +0000 Subject: [PATCH 3/6] Fix `show()` edge case --- src/show.luau | 33 ++++++++++++---------- test/tests.luau | 74 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 14 deletions(-) diff --git a/src/show.luau b/src/show.luau index e33393f..c4d87db 100644 --- a/src/show.luau +++ b/src/show.luau @@ -1,28 +1,33 @@ +local source = require "./source" local derive = require "./derive" +local effect = require "./effect" local untrack = require "./untrack" -local function show(source: () -> T?, component: (() -> T) -> U, fallback: (() -> U)?): () -> U? - local truthy = derive(function() - return not not source() +local function show(input: () -> T?, component: (() -> T) -> U, fallback: (() -> U)?): () -> U? + local filtered_input = source() + + effect(function() + local v = input() + if v then + filtered_input(v) + end end) - -- seemingly redundant derivation to extend the reactive graph so that - -- the propogation of the source's update is delayed, giving time for - -- the show() scope to be destroyed before potential effects registered - -- by the component can run when they should not run - -- todo: are there cases this method does not cover? - local derived = derive(function() - return source() + local input_is_truthy = derive(function() + return not not input() end) + -- todo: is this needed? + -- local filtered_input_is_truthy = derive(function() + -- return not not filtered_input() + -- end) + return derive(function() return - if truthy() then untrack(function() return component(derived :: () -> T) end) + if input_is_truthy() then untrack(function() return component(filtered_input :: () -> T) end) elseif fallback then untrack(fallback) else nil end) end -return show :: ( - ( (source: () -> T?, component: (() -> T) -> U) -> () -> U? ) -) +return show diff --git a/test/tests.luau b/test/tests.luau index 36a22b8..cd627fc 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -1153,6 +1153,80 @@ TEST("show()", wrap_root(function() CHECK(count == 3) end + do CASE "special strict case" + type Weapon = { + id: string, + enchant: string? + } + + vide.strict = true + + local count = 0 + local branch = 0 + + local weapon = source(nil :: Weapon?) + + effect(function() + weapon() + end) + + effect(function() + weapon() + end) + + show(weapon, function(weapon: () -> Weapon) + local enchant = function() return weapon().enchant end + + show(enchant, function(enchant: () -> string) + effect(function() + local e = enchant() + count += 1 + CHECK(e ~= nil) + if branch == 1 then + CHECK(e == "fire") + elseif branch == 2 then + CHECK(e == "poison") + end + end) + + return {} + end) + + return {} + end) + + effect(function() + weapon() + end) + + effect(function() + weapon() + end) + + branch = 1 + weapon { id = "1", enchant = "fire" } + CHECK(count == 8) + + branch = 2 + weapon { id = "1", enchant = "poison" } + CHECK(count == 10) + + weapon { id = "1", enchant = nil } + CHECK(count == 10) + + branch = 1 + weapon { id = "1", enchant = "fire" } + CHECK(count == 14) + + weapon(nil) + + branch = 2 + weapon { id = "1", enchant = "poison" } + CHECK(count == 22) + + vide.strict = false + end + do CASE "alt" -- todo: move test local visible = vide.source(true) local count = vide.source(0) From b7878753bd8fe131035b51887413b35334851444 Mon Sep 17 00:00:00 2001 From: aaron <83140718+centau@users.noreply.github.com> Date: Tue, 7 Jan 2025 00:06:39 +0000 Subject: [PATCH 4/6] Update `untrack()` type to allow no return --- src/untrack.luau | 2 +- test/tests.luau | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/untrack.luau b/src/untrack.luau index 577da25..6e15578 100644 --- a/src/untrack.luau +++ b/src/untrack.luau @@ -22,4 +22,4 @@ local function untrack(source: () -> T): T end end -return untrack +return untrack :: ( (fn: () -> T) -> T ) & ( (fn: () -> ()) -> () ) diff --git a/test/tests.luau b/test/tests.luau index cd627fc..e9365c2 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -1250,7 +1250,6 @@ TEST("show()", wrap_root(function() destroyed += 1 end) end) - return nil end) end) end) @@ -2863,7 +2862,6 @@ TEST("strict", wrap_root(function() vide.cleanup(function() count_2 += 1 end) return {} end) - return nil end) end) end) @@ -2890,7 +2888,6 @@ TEST("strict", wrap_root(function() vide.cleanup(function() count_2 += 1 end) return {} end) - return nil end) end) end) From 7064489a369521006135b5a0bd73bde548a631ef Mon Sep 17 00:00:00 2001 From: HarryXChen <51322624+HarryXChen3@users.noreply.github.com> Date: Thu, 16 Jan 2025 21:06:54 -0500 Subject: [PATCH 5/6] Fix typo in scope crash course docs (#46) --- docs/tut/crash-course/6-scope.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tut/crash-course/6-scope.md b/docs/tut/crash-course/6-scope.md index 7775647..e2cea69 100644 --- a/docs/tut/crash-course/6-scope.md +++ b/docs/tut/crash-course/6-scope.md @@ -7,7 +7,7 @@ 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. -Thre are two types of scopes: stable and reactive. +There are two types of scopes: stable and reactive. - A scope must be created within another scope. - Stable scopes never rerun. From 37e8e05206529ccc7e4f31710f1612254f2670f8 Mon Sep 17 00:00:00 2001 From: EwDev <74792428+ewd3v@users.noreply.github.com> Date: Sun, 23 Feb 2025 22:31:26 +0100 Subject: [PATCH 6/6] Support nesting Parent (#48) * allow nesting parent property * update changelog * fix mock instances Parent property not being a proxy * add tests Closes #47 --- CHANGELOG.md | 2 ++ src/apply.luau | 16 +++++++++++----- test/mock.luau | 35 +++++++++++++++++++---------------- test/tests.luau | 7 +++++++ 4 files changed, 39 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00f644f..2cde25a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). destroyed. - Error reporting should be improved with better formatting when effects invoke other effects and no more loss of stack traces. +- Nesting parent properties now work, and they are now also checked for + duplicates like other properties. ### Removed diff --git a/src/apply.luau b/src/apply.luau index 05ba98a..85020ff 100644 --- a/src/apply.luau +++ b/src/apply.luau @@ -23,6 +23,9 @@ type Cache = { Array<(Instance) -> ()> -- action callbacks >, + -- what to parent the instance to after running actions + parent: unknown, + -- cache to detect duplicate property setting at same nesting depth nested_debug: Map< number, -- depth @@ -47,6 +50,7 @@ local function borrow_cache(): Cache actions = setmetatable({} :: any, { -- lazy init __index = function(self, i) self[i] = {}; return self[i] end }), + parent = nil, nested_debug = setmetatable({} :: any, { __index = function(self, i: number) self[i] = {}; return self[i] end }), @@ -61,8 +65,6 @@ end local function process_properties(properties: Map, instance: Instance, cache: Cache, depth: number) for property, value in properties do - if property == "Parent" then continue end - if type(property) == "string" then if flags.strict then -- check for duplicate property assignment at nesting depth if cache.nested_debug[depth][property] then @@ -71,6 +73,11 @@ local function process_properties(properties: Map, instance: I cache.nested_debug[depth][property] = true end + if property == "Parent" then + cache.parent = value + continue + end + if type(value) == "function" then if typeof((instance :: any)[property]) == "RBXScriptSignal" then table.insert(cache.events, property) -- add event name to buffer @@ -106,9 +113,6 @@ local function apply(instance: T & Instance, properties: { [unknown]: unknown error "attempt to call a constructor returned by create() with no properties" end - -- queue parent assignment if any for last - local parent: unknown = properties.Parent - local caches = borrow_cache() local events = caches.events local actions = caches.actions @@ -135,6 +139,7 @@ local function apply(instance: T & Instance, properties: { [unknown]: unknown end end + local parent = caches.parent if parent then if type(parent) == "function" then implicit_effect.parent(instance, parent :: () -> Instance) @@ -145,6 +150,7 @@ local function apply(instance: T & Instance, properties: { [unknown]: unknown table.clear(events) for _, queued in next, actions do table.clear(queued) end + caches.parent = nil if flags.strict then table.clear(nested_debug) end table.clear(nested_stack) diff --git a/test/mock.luau b/test/mock.luau index 34564e6..a1f22a6 100644 --- a/test/mock.luau +++ b/test/mock.luau @@ -86,6 +86,9 @@ local Instance = {} :: any do local proxies = {} :: { [Data]: userdata? } setmetatable(proxies :: any, { __mode = "v" }) + -- allocate variables for the metamethods as __index and get_proxy cross refrences each other + local __index, __newindex + local function get_data(userdata: userdata): Data local function f(userdata: userdata): ProxyMT return getmetatable(userdata :: any) @@ -94,6 +97,19 @@ local Instance = {} :: any do return f(userdata).data end + local function get_proxy(data: Data): userdata + return proxies[data] or (function() + local userdata = newproxy(true) + local proxy = getmetatable(userdata) + proxy.proxy = userdata + proxy.data = data + proxy.__index = __index + proxy.__newindex = __newindex + proxies[data] = userdata + return userdata + end)() + end + local function is_instance(value: unknown): boolean local mt = getmetatable(value :: any) return mt and mt.data and mt.data.type == "Instance" @@ -101,16 +117,16 @@ local Instance = {} :: any do local methods = {} - local function __index(userdata: userdata, property: string): () + __index = function(userdata: userdata, property: string): () local data = get_data(userdata) return if methods[property] then methods[property] elseif property == "Name" then data.name - elseif property == "Parent" then data.parent + elseif property == "Parent" then (data.parent and get_proxy(data.parent)) elseif property == "Destroying" then data.destroying else data.properties[property] end - local function __newindex(userdata: userdata, property: string, value: unknown) + __newindex = function(userdata: userdata, property: string, value: unknown) local data = get_data(userdata) if property == "Name" then if type(value) ~= "string" then error("name must be a string", 2) end @@ -135,19 +151,6 @@ local Instance = {} :: any do end end - local function get_proxy(data: Data): userdata - return proxies[data] or (function() - local userdata = newproxy(true) - local proxy = getmetatable(userdata) - proxy.proxy = userdata - proxy.data = data - proxy.__index = __index - proxy.__newindex = __newindex - proxies[data] = userdata - return userdata - end)() - end - function Instance.new(class: string): Instance local data = { name = "UNNAMED", diff --git a/test/tests.luau b/test/tests.luau index e9365c2..14409bb 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -759,6 +759,13 @@ TEST("create()", wrap_root(function() CHECK(text.Text == "test") end + do CASE "set nested parent" + local frame = create "Frame" {} + local text = create "TextLavel" { { Parent = frame } } + CHECK(frame:GetChildren()[1] == text) + CHECK(text.Parent == frame) + end + do CASE "nested deferred" local text = create "TextLabel" { {