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,32 +22,64 @@ setmetatable(action_buffers :: any, {
end end
}) })
local function recurse(instance: Instance, properties: { [unknown]: unknown }) local nested_debug_cache: { [number]: { [string]: true } } = {}
for property, value in properties do
if type(value) == "table" then setmetatable(nested_debug_cache :: any, {
if is_action(value) then __index = function(_, i: number)
table.insert(action_buffers[(value :: any).priority], (value :: any).callback :: () -> ()) nested_debug_cache[i] = {}
else return nested_debug_cache[i]
recurse(instance, value :: {}) end
end })
elseif type(property) == "string" then
if type(value) == "function" then local nested_stack = {} :: { {} | number }
if typeof((instance :: any)[property]) == "RBXScriptSignal" then
event_buffer[property] = value :: () -> () 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 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 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 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()