diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 1aa4694..d1ae594 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -43,7 +43,7 @@ export default defineConfig({ { text: "Source", link: "/tut/crash-course/4-source" }, { text: "Derived Source", link: "/tut/crash-course/5-derived-source" }, { text: "Table Source", link: "/tut/crash-course/6-table-source" }, - { text: "Property Groups", link: "/tut/crash-course/7-property-groups" }, + { text: "Nested Properties", link: "/tut/crash-course/7-nested-properties" }, { text: "Actions", link: "/tut/crash-course/8-actions" }, ] }, diff --git a/docs/api/strict-mode.md b/docs/api/strict-mode.md index a0f826f..0b244f8 100644 --- a/docs/api/strict-mode.md +++ b/docs/api/strict-mode.md @@ -15,7 +15,8 @@ Currently, strict mode will: 2. Run watchers twice when a source updates. 3. Throw an error if yields occur where they are not allowed. 4. Checks for `indexes()` and `values()` returning primitive values. -5. Better error reporting and stack traces. +5. Checks for duplicate nested properties at same depth. +6. Better error reporting and stack traces. By rerunning sources and watchers, any side-effects are made more apparent. This also helps ensure that cleanups are being handled correctly. diff --git a/docs/tut/crash-course/7-property-groups.md b/docs/tut/crash-course/7-nested-properties.md similarity index 82% rename from docs/tut/crash-course/7-property-groups.md rename to docs/tut/crash-course/7-nested-properties.md index 1afea78..8da9d78 100644 --- a/docs/tut/crash-course/7-property-groups.md +++ b/docs/tut/crash-course/7-nested-properties.md @@ -1,4 +1,4 @@ -# Property Groups +# Nested Properties Often when creating components from existing components, you can find yourself repetitively passing through properties such as size or position. @@ -99,3 +99,22 @@ List { } } ``` + +Deeper nested properties are guaranteed to be set after shallower nested +properties, this can be used to create overridable default properties. + +```lua +local function CenteredList(props: Children & Layout) + return List { + Layout = { + props.Layout, + + -- can be overriden by `props.Layout` + AnchorPoint = Vector2.new(0.5, 0), + Position = UDim2.fromScale(0.5, 0) + }, + + Children = props.Children + } +end +``` diff --git a/src/apply.luau b/src/apply.luau index 252f0cf..dfcac2e 100644 --- a/src/apply.luau +++ b/src/apply.luau @@ -5,11 +5,12 @@ if not game then typeof = require "test/mock".typeof end -local graph = require(script.Parent.graph) -type Node = graph.Node - +local flags = require(script.Parent.flags) +local throw = require(script.Parent.throw) local bind = require(script.Parent.bind) local _, is_action = require(script.Parent.action)() +local graph = require(script.Parent.graph) +type Node = graph.Node local event_buffer: { [string]: () -> () } = {} local action_buffers = {} :: { { () -> () } } @@ -21,32 +22,64 @@ setmetatable(action_buffers :: any, { end }) -local function recurse(instance: Instance, properties: { [unknown]: unknown }) - for property, value in properties do - if type(value) == "table" then - if is_action(value) then - table.insert(action_buffers[(value :: any).priority], (value :: any).callback :: () -> ()) - else - recurse(instance, value :: {}) - end - elseif type(property) == "string" then - if type(value) == "function" then - if typeof((instance :: any)[property]) == "RBXScriptSignal" then - event_buffer[property] = value :: () -> () +local nested_debug_cache: { [number]: { [string]: true } } = {} + +setmetatable(nested_debug_cache :: any, { + __index = function(_, i: number) + nested_debug_cache[i] = {} + return nested_debug_cache[i] + end +}) + +local nested_stack = {} :: { {} | number } + +local function process(instance: Instance, properties: { [unknown]: unknown }) + local strict = flags.strict + + table.clear(nested_stack) + if strict then table.clear(nested_debug_cache) end + + local depth = 1 + + repeat + for property, value in properties do + if type(value) == "table" then + if is_action(value) then + table.insert(action_buffers[(value :: any).priority], (value :: any).callback :: () -> ()) else - bind.property(instance, property, value :: () -> ()) + table.insert(nested_stack, depth + 1) + table.insert(nested_stack, value :: {}) + end + elseif type(property) == "string" then + if strict then + if nested_debug_cache[depth][property] then + throw(`duplicate property {property} at depth {depth}`) + end + nested_debug_cache[depth][property] = true + end + + if type(value) == "function" then + if typeof((instance :: any)[property]) == "RBXScriptSignal" then + event_buffer[property] = value :: () -> () + else + bind.property(instance, property, value :: () -> ()) + end + else + (instance :: any)[property] = value + end + elseif type(property) == "number" then + if type(value) == "function" then + bind.children(instance, value :: () -> { Instance }) + else + (value :: Instance).Parent = instance end - else - (instance :: any)[property] = value - end - elseif type(property) == "number" then - if type(value) == "function" then - bind.children(instance, value :: () -> { Instance }) - else - (value :: Instance).Parent = instance end end - end + + properties = table.remove(nested_stack) :: {} + depth = table.remove(nested_stack) :: number + + until not properties end local function apply(instance: T & Instance, properties: { [unknown]: unknown }): T @@ -58,7 +91,7 @@ local function apply(instance: T & Instance, properties: { [unknown]: unknown table.clear(buffer) end - recurse(instance, properties) + process(instance, properties) for event, fn in next, event_buffer do (instance :: any)[event]:Connect(fn) diff --git a/test/tests.luau b/test/tests.luau index f3c965e..338fcf7 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -596,6 +596,20 @@ TEST("create()", function() CHECK(text.Text == "test") end + do CASE "nested precedence" + local text = create "TextLabel" { + Group = { + Text = "1", + + { + Text = "2" + } + } + } + + CHECK(text.Text == "2") + end + do CASE "independent" local frame = create "Frame" CHECK(frame {} ~= frame {}) @@ -1153,6 +1167,7 @@ end) TEST("strict", function() vide.strict = true + local create = vide.create local source = vide.source local derive = vide.derive local watch = vide.watch @@ -1231,6 +1246,34 @@ TEST("strict", function() CHECK(not ok) end + + do CASE "duplicate properties" + local ok = pcall(function() + create "TextLabel" { + { + Name = "foo" + }, + { + Name = "bar" + } + } + end) + + CHECK(not ok) + + ok = pcall(function() + create "TextLabel" { + { + Name = "foo", + { + Name = "bar" + } + } + } + end) + + CHECK(ok) + end end) local ok = FINISH()