--!strict --[[ A special key for property tables, which parents any given descendants into an instance. ]] local PubTypes = require "../PubTypes" local External = require "../External" local logWarn = require "../Logging/logWarn" local Observer = require "../State/Observer" local peek = require "../State/peek" local isState = require "../State/isState" local typeof = require "../../../Modules/Polyfill/typeof" type Set = { [T]: boolean } -- Experimental flag: name children based on the key used in the [Children] table local EXPERIMENTAL_AUTO_NAMING = false local Children = {} Children.type = "SpecialKey" Children.kind = "Children" Children.stage = "descendants" function Children:apply( propValue: any, applyTo: Instance, cleanupTasks: { PubTypes.Task } ) local newParented: Set = {} local oldParented: Set = {} -- save disconnection functions for state object observers local newDisconnects: { [PubTypes.StateObject]: () -> () } = {} local oldDisconnects: { [PubTypes.StateObject]: () -> () } = {} local updateQueued = false local queueUpdate: () -> () -- Rescans this key's value to find new instances to parent and state objects -- to observe for changes; then unparents instances no longer found and -- disconnects observers for state objects no longer present. local function updateChildren() if not updateQueued then return -- this update may have been canceled by destruction, etc. end updateQueued = false oldParented, newParented = newParented, oldParented oldDisconnects, newDisconnects = newDisconnects, oldDisconnects -- table.clear(newParented) for i, _ in pairs(newParented) do newParented[i] = nil end -- table.clear(newDisconnects) for i, _ in pairs(newDisconnects) do newDisconnects[i] = nil end local function processChild(child: any, autoName: string?) local childType = typeof(child) if childType == "Instance" then -- case 1; single instance newParented[child] = true if oldParented[child] == nil then -- wasn't previously present -- TODO: check for ancestry conflicts here child.Parent = applyTo else -- previously here; we want to reuse, so remove from old -- set so we don't encounter it during unparenting oldParented[child] = nil end if EXPERIMENTAL_AUTO_NAMING and autoName ~= nil then child.Name = autoName end elseif isState(child) then -- case 2; state object local value = peek(child) -- allow nil to represent the absence of a child if value ~= nil then processChild(value, autoName) end local disconnect = oldDisconnects[child] if disconnect == nil then -- wasn't previously present disconnect = Observer(child):onChange(queueUpdate) else -- previously here; we want to reuse, so remove from old -- set so we don't encounter it during unparenting oldDisconnects[child] = nil end newDisconnects[child] = disconnect elseif childType == "table" then -- case 3; table of objects for key, subChild in pairs(child) do local keyType = typeof(key) local subAutoName: string? = nil if keyType == "string" then subAutoName = key elseif keyType == "number" and autoName ~= nil then subAutoName = autoName .. "_" .. key end processChild(subChild, subAutoName) end else logWarn("unrecognisedChildType", childType) end end if propValue ~= nil then -- `propValue` is set to nil on cleanup, so we don't process children -- in that case processChild(propValue) end -- unparent any children that are no longer present for oldInstance in pairs(oldParented) do oldInstance.Parent = nil end -- disconnect observers which weren't reused for _, disconnect in pairs(oldDisconnects) do disconnect() end end queueUpdate = function() if not updateQueued then updateQueued = true External.doTaskDeferred(updateChildren) end end table.insert(cleanupTasks, function() propValue = nil updateQueued = true updateChildren() end) -- perform initial child parenting updateQueued = true updateChildren() end return Children :: PubTypes.SpecialKey