Update property nesting

Deeper nested properties guaranteed to be processed after shallower
nested properties.

Also added strict mode checks for duplicate nested properties.
This commit is contained in:
Aaron Smith 2023-08-14 11:42:16 +01:00
parent ddb88cfdf8
commit a769139136
5 changed files with 125 additions and 29 deletions

View file

@ -43,7 +43,7 @@ export default defineConfig({
{ text: "Source", link: "/tut/crash-course/4-source" }, { text: "Source", link: "/tut/crash-course/4-source" },
{ text: "Derived Source", link: "/tut/crash-course/5-derived-source" }, { text: "Derived Source", link: "/tut/crash-course/5-derived-source" },
{ text: "Table Source", link: "/tut/crash-course/6-table-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" }, { text: "Actions", link: "/tut/crash-course/8-actions" },
] ]
}, },

View file

@ -15,7 +15,8 @@ Currently, strict mode will:
2. Run watchers twice when a source updates. 2. Run watchers twice when a source updates.
3. Throw an error if yields occur where they are not allowed. 3. Throw an error if yields occur where they are not allowed.
4. Checks for `indexes()` and `values()` returning primitive values. 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. By rerunning sources and watchers, any side-effects are made more apparent.
This also helps ensure that cleanups are being handled correctly. This also helps ensure that cleanups are being handled correctly.

View file

@ -1,4 +1,4 @@
# Property Groups # Nested Properties
Often when creating components from existing components, you can find yourself Often when creating components from existing components, you can find yourself
repetitively passing through properties such as size or position. 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
```

View file

@ -5,11 +5,12 @@ if not game then
typeof = require "test/mock".typeof typeof = require "test/mock".typeof
end end
local graph = require(script.Parent.graph) local flags = require(script.Parent.flags)
type Node<T> = graph.Node<T> local throw = require(script.Parent.throw)
local bind = require(script.Parent.bind) local bind = require(script.Parent.bind)
local _, is_action = require(script.Parent.action)() local _, is_action = require(script.Parent.action)()
local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T>
local event_buffer: { [string]: () -> () } = {} local event_buffer: { [string]: () -> () } = {}
local action_buffers = {} :: { { () -> () } } local action_buffers = {} :: { { () -> () } }
@ -21,15 +22,42 @@ setmetatable(action_buffers :: any, {
end end
}) })
local function recurse(instance: Instance, properties: { [unknown]: unknown }) 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 for property, value in properties do
if type(value) == "table" then if type(value) == "table" then
if is_action(value) then if is_action(value) then
table.insert(action_buffers[(value :: any).priority], (value :: any).callback :: () -> ()) table.insert(action_buffers[(value :: any).priority], (value :: any).callback :: () -> ())
else else
recurse(instance, value :: {}) table.insert(nested_stack, depth + 1)
table.insert(nested_stack, value :: {})
end end
elseif type(property) == "string" then 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 type(value) == "function" then
if typeof((instance :: any)[property]) == "RBXScriptSignal" then if typeof((instance :: any)[property]) == "RBXScriptSignal" then
event_buffer[property] = value :: () -> () event_buffer[property] = value :: () -> ()
@ -47,6 +75,11 @@ local function recurse(instance: Instance, properties: { [unknown]: unknown })
end end
end end
end end
properties = table.remove(nested_stack) :: {}
depth = table.remove(nested_stack) :: number
until not properties
end end
local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown }): T local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown }): T
@ -58,7 +91,7 @@ local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown
table.clear(buffer) table.clear(buffer)
end end
recurse(instance, properties) process(instance, properties)
for event, fn in next, event_buffer do for event, fn in next, event_buffer do
(instance :: any)[event]:Connect(fn) (instance :: any)[event]:Connect(fn)

View file

@ -596,6 +596,20 @@ TEST("create()", function()
CHECK(text.Text == "test") CHECK(text.Text == "test")
end end
do CASE "nested precedence"
local text = create "TextLabel" {
Group = {
Text = "1",
{
Text = "2"
}
}
}
CHECK(text.Text == "2")
end
do CASE "independent" do CASE "independent"
local frame = create "Frame" local frame = create "Frame"
CHECK(frame {} ~= frame {}) CHECK(frame {} ~= frame {})
@ -1153,6 +1167,7 @@ end)
TEST("strict", function() TEST("strict", function()
vide.strict = true vide.strict = true
local create = vide.create
local source = vide.source local source = vide.source
local derive = vide.derive local derive = vide.derive
local watch = vide.watch local watch = vide.watch
@ -1231,6 +1246,34 @@ TEST("strict", function()
CHECK(not ok) CHECK(not ok)
end 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) end)
local ok = FINISH() local ok = FINISH()