Skip to content

Instantly share code, notes, and snippets.

@cxmeel
Last active October 15, 2023 13:58
Show Gist options
  • Save cxmeel/2fd7092ed359fc769abc56a38ec74441 to your computer and use it in GitHub Desktop.
Save cxmeel/2fd7092ed359fc769abc56a38ec74441 to your computer and use it in GitHub Desktop.

Create.lua

Creates a new Instance with a list of properties, attributes, tags and events. Based on the legacy RbxUtility.Create method.

Creation flow:

  • Create Instance (skipped when using Hydration)
  • Apply properties to the Instance
  • Apply CollectionService tags to the Instance
  • Reparent any child Instances to the new Instance
  • Connect events, such as attribute change events, property change events, and Instance events (such as DescendantAdded)
  • Set the Parent of the Instance

Warning: The Parent property is overridden for children. If a child specifies a Parent, it will be ignored, and the child will be parented to the parent in the Create tree.


Table of Contents

  1. Install
  2. API Overview
  3. Introduction
  4. Tags and Attributes
  5. Events and Signals
  6. Children
  7. Constructors
  8. Hydration
  9. Unstable Version

Install

Manually

Download and insert, or copy and paste Create.lua into a ModuleScript in Studio.

Command Bar

Paste and run the following line of code in Studio's command bar.

local hs=game:GetService"HttpService";local he=hs.HttpEnabled;hs.HttpEnabled=true;local src=hs:GetAsync"https://gist.github.com/cxmeel/2fd7092ed359fc769abc56a38ec74441/raw/Create.lua";local m=Instance.new"ModuleScript";m.Name="Create";m.Source=src;m.Parent=game:GetService"ReplicatedStorage";hs.HttpEnabled=he
Command overview
  • Get the current state of HttpService.HttpEnabled
  • Set HttpService.HttpEnabled to true
  • Download the latest source from this gist
  • Create a new ModuleScript with name "Create"
  • Set the ModuleScript's contents to the previously downloaded source
  • Parent ModuleScript to ReplicatedStorage
  • Set HttpService.HttpEnabled back to its original state

API Overview

  • Create<T>(className: string)(props: { [any]: any }) -> T - Example
  • Create.Hydrate<T>(target: T, props: { [any]: any }) -> T - Example
  • Create.Attribute(name: string) -> Symbol - Example
  • Create.Event(name: string) -> Symbol - Example
  • Create.Tag = Symbol - Example
  • Create.Changed(property: string) -> Symbol - Example
  • Create.Changed.Attribute(name: string) -> Symbol - Example

Introduction

Create always returns a physical Instance. Props are static; updating the props will not update the Instance. If this is desired, you may be looking for libraries like Roact or Fusion.

local TextBox = Create "TextBox" {
    ClearTextOnFocus = false,
    BackgroundColor3 = Color3.fromHex("#1a1a1a"),
    TextColor3 = Color3.fromHex("#fafafa"),
    PlaceholderText = "Enter message",
    Parent = PlayerGui.ScreenGui,
    Text = "",

    [Create.Changed "Text"] = function(rbx: TextBox, prev: string)
        print("Text was changed from", prev, "to", rbx.Text)
    end,
}

Tags and Attributes

Attributes and Tags can be applied to an Instance. Tags can either be a string or an array of strings.

local Create = require(path.to.create)
local Attribute, Tags = Create.Attribute, Create.Tag

Create "Configuration" {
    Name = ".config",

    [Attribute "Enabled"] = true,
    [Tags] = { "Configuration", "PluginSettings" },
}

Events and Signals

Create can also connect events to listen for updates on attributes or properties, or connect Instance events.

local Changed, Event = Create.Changed, Create.Event

Create "TextBox" {
    [Attribute "Enabled"] = true,

    [Event "ChildAdded"] = function(rbx: TextBox, child: Instance)
        print("Child", child.Name, "was added to TextBox", rbx.Name)
    end,

    [Changed "Text"] = function(rbx: TextBox, prev: string)
        print("Text was changed from", prev, "to", rbx.Text)
    end,

    [Changed.Attr "Enabled"] = function(rbx: TextBox, prev: boolean?)
        print("TextBox Enabled changed from", prev, "to", rbx:GetAttribute("Enabled"))
        rbx.TextEditable = rbx:GetAttribute("Enabled")
    end,
}

Children

Children can also be specified in the Create props.

Create "Frame" {
    Size = UDim2.fromScale(1, 1),

    Create "TextLabel" {
        Text = "Hello!",
    },

    -- Reparent Baseplate to the new Frame
    workspace.Baseplate,
}

Warning: The Parent property is overridden for children. If a child specifies a Parent, it will be ignored, and the child will be parented to the parent in the Create tree.

Constructors

Each Instance may have a single constructor method. This is called once the Instance has been created and all props have been applied (excluding Parent), and before events are connected.

local reference = nil

Create "Frame" {
    BackgroundColor3 = Color3.fromHex("#00a2ff"),

    [Create] = function(rbx: Frame)
        reference = rbx
    end,
}

Hydration

Hydrate is a function which allows you to bind events, properties and children to already existing Instances. This is good when you want to connect events to a service, for example:

local GuiService = game:GetService("GuiService")
local Lighting = game:GetService("Lighting")

local Hydrate, Event = Create.Hydrate, Create.Event

local blur = Create "BlurEffect" {
  Size = 0,
  Parent = Lighting,
}

Hydrate(GuiService) {
  [Event "MenuOpened"] = function()
    blur.Size = 24
  end,
    
  [Event "MenuClosed"] = function()
    blur.Size = 0
  end,
}

Unstable Version

The unstable version is not designed for production code. Use at your own risk.

local hs=game:GetService"HttpService";local sl=game:GetService"Selection";local he=hs.HttpEnabled;hs.HttpEnabled=true;local src=hs:GetAsync"https://gist.github.com/cxmeel/2fd7092ed359fc769abc56a38ec74441/raw/Create.Unstable.lua";local m=Instance.new"ModuleScript";m.Name="Create";m.Source=src;m.Parent=sl:Get()[1] or game:GetService"ReplicatedStorage";hs.HttpEnabled=he;sl:Set({m})

Key Differences

  • More strongly typed
  • Implements EventGroup and Children SpecialKeys
  • Uses Instance:AddTag instead of CollectionService:AddTag to assign tags

Children SpecialKey

The Children SpecialKey is an alternative to storing child Instances inside the root table. This is purely for aesthetics and is functionally the same as using the root table. The difference between using the Children SpecialKey and the root table is that the Children SpecialKey does not care how you index child Instances; however, the index does not have any affect on the creation or properties of the Instance.

local Children = Create.Children

Create "ScreenGui" {
  Name = "HelloWorld",
  
  [Children] = {
    SomeLabel = Create "TextLabel" {
      Text = "Hello, world!",
    },
  },
}

EventGroups

EventGroups are a way of connecting multiple signals to a single function. EventGroups support the following signals (in the form of SpecialKeys):

  • Property changed events
  • Attribute changed events
  • Instance events (e.g. .ChildAdded, .Destroying)
  • Constructor SpecialKeys
local EventGroup = Create.EventGroup
local Changed = Create.Changed
local Event = Create.Event

Create "Part" {
  [EventGroup { Changed "Position", Event "Touched" }] = function(rbx: Part)
    -- called when Part.Position is changed
    -- or when Part.Touched
  end,
}

Bear in mind that EventGroups do not distinguish between signals, i.e. using the example above, you would not be able to tell within the callback whether it was the position change event or the touch event that triggered the callback. If you need access to the callback parameters from these events, you should not group them together.

--!strict
local CollectionService = game:GetService("CollectionService")
local Creator = {}
type PropsType = { [any]: any }
local function SymbolicKey(name: string, valueType: string?)
return function(value: any)
if valueType ~= nil then
assert(
typeof(value) == valueType,
`SymbolicKey "{name}" expected value to be of type "{valueType}", but got {typeof(value)} ({tostring(value)})`
)
end
return {
type = name,
value = value,
}
end
end
local Attribute = SymbolicKey("ATTRIBUTE", "string")
local AttributeChanged = SymbolicKey("ATTRIBUTE_CHANGED", "string")
local Changed = SymbolicKey("CHANGED", "string")
local Event = SymbolicKey("EVENT", "string")
local Tag = SymbolicKey("TAG")(nil)
local Create = nil
local function ApplyProps(target: Instance, props: PropsType)
local targetInstance = target :: any
local events = { attribute = {}, property = {}, event = {} }
local constructor = nil
local parent = nil
props = props or {}
for key, value in props do
local keyType, valueType = typeof(key), typeof(value)
if keyType == "string" then
if key == "Parent" then
assert(
valueType == "Instance",
`Parent expects an "Instance", but got {typeof(value)} ({tostring(value)})`
)
parent = value
else
targetInstance[key] = value
end
continue
end
if keyType == "number" then
assert(
valueType == "Instance",
`Numeric keys expect an "Instance", but got {typeof(value)} ({tostring(value)})`
)
value.Parent = targetInstance
continue
end
if key == Creator or key == Create then
assert(constructor == nil, "Only one constructor may exist per Instance")
assert(
valueType == "function",
`Create expected constructor to be of type "function", but got {typeof(value)} ({tostring(value)})`
)
constructor = value
continue
end
if keyType == "table" then
local symbolType, symbolValue = key.type, key.value
if symbolType == "ATTRIBUTE" then
targetInstance:SetAttribute(symbolValue, value)
elseif symbolType == "ATTRIBUTE_CHANGED" then
events.attribute[symbolValue] = value
elseif symbolType == "CHANGED" then
events.property[symbolValue] = value
elseif symbolType == "EVENT" then
events.event[symbolValue] = value
elseif symbolType == "TAG" then
assert(
valueType == "string" or valueType == "table",
`Tag expects a "string | \{string}", but got {typeof(value)} ({tostring(value)})`
)
if valueType == "string" then
value = { value }
end
for _, tagName in value do
CollectionService:AddTag(targetInstance, tagName)
end
end
end
end
if constructor ~= nil then
constructor(targetInstance)
end
for watchedAttribute, callback in events.attribute do
local prevValue = targetInstance:GetAttribute(watchedAttribute)
targetInstance:GetAttributeChangedSignal(watchedAttribute):Connect(function(...)
callback(targetInstance, prevValue, ...)
prevValue = targetInstance:GetAttribute(watchedAttribute)
end)
end
for watchedProperty, callback in events.property do
local prevValue = targetInstance[watchedProperty]
targetInstance:GetPropertyChangedSignal(watchedProperty):Connect(function(...)
callback(targetInstance, prevValue, ...)
prevValue = targetInstance[watchedProperty]
end)
end
for eventName, callback in events.event do
targetInstance[eventName]:Connect(function(...)
callback(targetInstance, ...)
end)
end
if parent ~= nil then
targetInstance.Parent = parent
end
end
function Create<T>(className: string): (props: PropsType) -> T
assert(
typeof(className) == "string",
`Create expected className to be of type "string", but got {typeof(className)} ({tostring(className)})`
)
return function(props)
local instance = Instance.new(className)
ApplyProps(instance, props)
return instance :: any
end
end
local function Hydrate<T>(target: T): (props: PropsType) -> T
assert(
typeof(target) == "Instance",
`Hydrate expected target to be of type "Instance", but got {typeof(target)} ({tostring(target)})`
)
return function(props)
ApplyProps(target, props)
return target
end
end
setmetatable(Creator, {
__call = function(_, ...)
return Create(...)
end,
})
Creator.Create = Create
Creator.Hydrate = Hydrate
Creator.Attribute = Attribute
Creator.Attr = Attribute
Creator.Event = Event
Creator.Tag = Tag
Creator.Tags = Tag
Creator.Changed = setmetatable({
Attribute = AttributeChanged,
Attr = AttributeChanged,
}, {
__call = function(_, ...)
return Changed(...)
end,
})
return Creator
--!strict
local Creator = {}
type PropsType = { [any]: any }
type SymbolicKey<T> = { type: string, value: T }
type PublicSymbolicKey<T> = (value: T) -> SymbolicKey<T>
local function SymbolicKey(name: string, valueType: string?)
return function(value: any)
if valueType ~= nil and valueType ~= "*" then
assert(
typeof(value) == valueType,
`SymbolicKey "{name}" expects value "{valueType}", but got {typeof(value)} ({tostring(value)})`
)
end
return {
type = name,
value = value,
}
end
end
local Attribute = SymbolicKey("ATTRIBUTE", "string") :: PublicSymbolicKey<string>
local AttributeChanged = SymbolicKey("ATTRIBUTE_CHANGED", "string") :: PublicSymbolicKey<string>
local Changed = SymbolicKey("CHANGED", "string") :: PublicSymbolicKey<string>
local Event = SymbolicKey("EVENT", "string") :: PublicSymbolicKey<string>
local Tag = SymbolicKey("TAG")(nil) :: SymbolicKey<nil>
local EventGroup = SymbolicKey("EVENT_GROUP", "*") :: PublicSymbolicKey<{ SymbolicKey<string> | Create | Creator }>
local Children = SymbolicKey("CHILDREN")(nil) :: SymbolicKey<nil>
local Create = nil
local function ApplyProps(target: Instance, props: PropsType)
local targetInstance = target :: any
local events = { attribute = {}, property = {}, event = {} }
local constructor = nil
local parent = nil
props = props or {}
for key, value in props do
local keyType, valueType = typeof(key), typeof(value)
if keyType == "string" then
if key == "Parent" then
assert(
valueType == "Instance",
`Parent expects "Instance", but got {typeof(value)} ({tostring(value)})`
)
parent = value
else
targetInstance[key] = value
end
continue
end
if keyType == "number" then
assert(
valueType == "Instance",
`Numeric keys expects "Instance", but got {typeof(value)} ({tostring(value)})`
)
value.Parent = targetInstance
continue
end
if key == Creator or key == Create then
assert(constructor == nil, "Only one constructor may exist per Instance")
assert(
valueType == "function",
`Create expected constructor to be of type "function", but got {typeof(value)} ({tostring(value)})`
)
constructor = value
continue
end
if keyType == "table" then
local symbolType, symbolValue = key.type, key.value
if symbolType == "CHILDREN" then
assert(
valueType == "table",
`Children expects "\{Instance}", but got {valueType} ({tostring(value)})`
)
for _, instance in value do
assert(
typeof(instance) == "Instance",
`Children expects "\{Instance}", but got {typeof(value)} ({tostring(value)})`
)
instance.Parent = targetInstance
end
end
if symbolType == "ATTRIBUTE" then
targetInstance:SetAttribute(symbolValue, value)
end
if symbolType == "ATTRIBUTE_CHANGED" then
events.attribute[symbolValue] = value
end
if symbolType == "CHANGED" then
events.property[symbolValue] = value
end
if symbolType == "EVENT" then
events.event[symbolValue] = value
end
if symbolType == "TAG" then
assert(
valueType == "string" or valueType == "table",
`Tag expects "string | \{string}", but got {valueType} ({tostring(value)})`
)
if valueType == "string" then
value = { value }
end
for _, tagName in value do
targetInstance:AddTag(tagName)
end
end
if symbolType == "EVENT_GROUP" then
assert(
typeof(symbolValue) == "table",
`EventGroup expects "\{SpecialKeys}" (i.e. Event, Changed, Changed.Attribute), but got {valueType} ({tostring(value)})`
)
assert(
valueType == "function",
`EventGroup expects "function", but got {valueType} ({tostring(value)})`
)
for _, groupSymbol in symbolValue do
if groupSymbol == Create or groupSymbol == Creator then
assert(constructor == nil, "Only one constructor may exist per Instance")
constructor = value
continue
end
local groupSymbolType, groupSymbolValue = groupSymbol.type, groupSymbol.value
if groupSymbolType == "ATTRIBUTE_CHANGED" then
events.attribute[groupSymbolValue] = value
end
if groupSymbolType == "CHANGED" then
events.property[groupSymbolValue] = value
end
if groupSymbolType == "EVENT" then
events.event[groupSymbolValue] = value
end
end
end
end
end
if constructor ~= nil then
constructor(targetInstance)
end
for watchedAttribute, callback in events.attribute do
local prevValue = targetInstance:GetAttribute(watchedAttribute)
targetInstance:GetAttributeChangedSignal(watchedAttribute):Connect(function(...)
callback(targetInstance, prevValue, ...)
prevValue = targetInstance:GetAttribute(watchedAttribute)
end)
end
for watchedProperty, callback in events.property do
local prevValue = targetInstance[watchedProperty]
targetInstance:GetPropertyChangedSignal(watchedProperty):Connect(function(...)
callback(targetInstance, prevValue, ...)
prevValue = targetInstance[watchedProperty]
end)
end
for eventName, callback in events.event do
targetInstance[eventName]:Connect(function(...)
callback(targetInstance, ...)
end)
end
if parent ~= nil then
targetInstance.Parent = parent
end
end
function Create<T>(className: string): (props: PropsType) -> T
assert(
typeof(className) == "string",
`Create expected className to be of type "string", but got {typeof(className)} ({tostring(className)})`
)
return function(props)
local instance = Instance.new(className)
ApplyProps(instance, props)
return instance :: any
end
end
local function Hydrate<T>(target: T): (props: PropsType) -> T
assert(
typeof(target) == "Instance",
`Hydrate expected target to be of type "Instance", but got {typeof(target)} ({tostring(target)})`
)
return function(props)
ApplyProps(target, props)
return target
end
end
setmetatable(Creator, {
__call = function(_, ...)
return Create(...)
end,
})
Creator.Create = Create
Creator.Hydrate = Hydrate
Creator.Attribute = Attribute
Creator.Attr = Attribute
Creator.Event = Event
Creator.Tag = Tag
Creator.Tags = Tag
Creator.EventGroup = EventGroup
Creator.Children = Children
Creator.Changed = setmetatable({
Attribute = AttributeChanged,
Attr = AttributeChanged,
}, {
__call = function(_, ...)
return Changed(...)
end,
})
type Create = typeof(Create)
type Creator = typeof(Creator)
return Creator
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment