Skip to content

Instantly share code, notes, and snippets.

@cameronpcampbell
Created January 28, 2025 07:09
Show Gist options
  • Save cameronpcampbell/b856b1b8ce8fe3d3e202403612aa0140 to your computer and use it in GitHub Desktop.
Save cameronpcampbell/b856b1b8ce8fe3d3e202403612aa0140 to your computer and use it in GitHub Desktop.
--!strict
--!nolint LocalUnused
--!nolint LocalShadow
local task = nil -- Disable usage of Roblox's task scheduler
--[[
A special key for property tables, which parents any given descendants into
an instance.
]]
local Package = script.Parent.Parent["[email protected]"].fusion
local Types = require(Package.Types)
local External = require(Package.External)
local Observer = require(Package.Graph.Observer)
local peek = require(Package.State.peek)
local castToState = require(Package.State.castToState)
local doCleanup = require(Package.Memory.doCleanup)
type Set<T> = {[T]: unknown}
-- Experimental flag: name children based on the key used in the [Children] table
local EXPERIMENTAL_AUTO_NAMING = false
local function ChildrenTransform(
transformer: (scope: Types.Scope, toTransform: unknown) -> Types.Child?
): Types.SpecialKey
return {
type = "SpecialKey",
kind = "Children",
stage = "descendants",
apply = function(
self: Types.SpecialKey,
scope: Types.Scope<unknown>,
value: unknown,
applyTo: Instance
)
local newParented: Set<Instance> = {}
local oldParented: Set<Instance> = {}
-- save scopes for state object observers
local newScopes: {[Types.StateObject<unknown>]: Types.Scope<unknown>} = {}
local oldScopes: {[Types.StateObject<unknown>]: Types.Scope<unknown>} = {}
-- 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()
oldParented, newParented = newParented, oldParented
oldScopes, newScopes = newScopes, oldScopes
local function processChild(
child: unknown,
autoName: string?
)
local childType = typeof(child)
if childType == "Instance" then
-- case 1; single instance
local child = child :: 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 castToState(child) then
-- case 2; state object
local child = child :: Types.StateObject<unknown>
local value = peek(child)
-- allow nil to represent the absence of a child
if value ~= nil then
-- We need to transform the value since it isn't an instance.
if typeof(value) ~= "Instance" then
local transformedValue = transformer(scope, value)
if typeof(transformedValue) == "Instance" then
processChild(transformedValue, autoName)
else
External.logWarn("unrecognisedChildType", typeof(value))
end
-- proccess the child without transforming since it is already
-- an instance.
else
processChild(value, autoName)
end
end
local childScope = oldScopes[child]
if childScope == nil then
-- wasn't previously present
childScope = {}
Observer(childScope, child):onChange(updateChildren)
else
-- previously here; we want to reuse, so remove from old
-- set so we don't encounter it during unparenting
oldScopes[child] = nil
end
newScopes[child] = childScope
elseif childType == "table" then
-- case 3; table of objects
local child = child :: {[unknown]: unknown}
for key, subChild in pairs(child) do
local keyType = typeof(key)
local subAutoName: string? = nil
if keyType == "string" then
local key = key :: string
subAutoName = key
elseif keyType == "number" and autoName ~= nil then
local key = key :: number
subAutoName = autoName .. "_" .. key
end
processChild(subChild, subAutoName)
end
else
local transformedChild = transformer(scope, child)
if typeof(transformedChild) == "Instance" then
processChild(transformedChild, autoName)
else
External.logWarn("unrecognisedChildType", childType)
end
end
end
if value ~= nil then
-- `propValue` is set to nil on cleanup, so we don't process children
-- in that case
processChild(value)
end
-- unparent any children that are no longer present
for oldInstance in pairs(oldParented) do
oldInstance.Parent = nil
end
table.clear(oldParented)
-- disconnect observers which weren't reused
for oldState, childScope in pairs(oldScopes) do
doCleanup(childScope)
end
table.clear(oldScopes)
end
table.insert(scope, function()
value = nil
updateChildren()
end)
-- perform initial child parenting
updateChildren()
end
} :: Types.SpecialKey
end
return ChildrenTransform
--[[
Modified from Fusion's src/Instances/Children file.
Fusion's License below:
MIT License
Copyright (c) 2024 Daniel P H Fox
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
@cameronpcampbell
Copy link
Author

A fork of Fusion's Children SpecialKey which allows you to transform non-instance children into instances.

Example usage

local Fusion = require(Path.To.Fusion)
local ChildrenTransform = require(Path.To.FusionChildrenTransform)

local ChildrenTransformStringToTextLabel = ChildrenTransform(
    function(scope: Fusion.Scope, value: unknown): Fusion.Child?
        if type(value) == "string" then
            return New(scope, "TextLabel") { Text = value }
        end
        
        return nil
    end
)

local Scope = Fusion.scoped(Fusion)

Scope:New "ImageButton" {
    [ChildrenTransformStringToTextLabel] = "Awesome Button"
}

Scope:New "ImageButton" {
    [ChildrenTransformStringToTextLabel] = {
        Scope:New "ImageLabel" { Image = "rbxassetid://116869494711779" }
        "Awesome Button",
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment