Component
Bind components to Roblox instances using the Component class and CollectionService tags.
To avoid confusion of terms:
Component
refers to this module.Component Class
(e.g.MyComponent
through this documentation) refers to a class created viaComponent.new
Component Instance
refers to an instance of a component class.Roblox Instance
refers to the Roblox instance to which the component instance is bound.
Methods and properties are tagged with the above terms to help clarify the level at which they are used.
Types
ExtensionFn
type
ExtensionFn =
(
component
)
→
(
)
ExtensionShouldFn
type
ExtensionShouldFn =
(
component
)
→
boolean
Extension
interface
Extension {
ShouldExtend:
ExtensionShouldFn?
ShouldConstruct:
ExtensionShouldFn?
Constructing:
ExtensionFn?
Constructed:
ExtensionFn?
Starting:
ExtensionFn?
Started:
ExtensionFn?
Stopping:
ExtensionFn?
Stopped:
ExtensionFn?
Methods:
{
[
string
]
:
function
}
?
}
An extension allows the ability to extend the behavior of components. This is useful for adding injection systems or extending the behavior of components by wrapping around component lifecycle methods.
The ShouldConstruct
function can be used to indicate
if the component should actually be created. This must
return true
or false
. A component with multiple
ShouldConstruct
extension functions must have them all
return true
in order for the component to be constructed.
The ShouldConstruct
function runs before all other
extension functions and component lifecycle methods.
The ShouldExtend
function can be used to indicate if
the extension itself should be used. This can be used in
order to toggle an extension on/off depending on whatever
logic is appropriate. If no ShouldExtend
function is
provided, the extension will always be used if provided
as an extension to the component.
As an example, an extension could be created to simply log when the various lifecycle stages run on the component:
local Logger = {}
function Logger.Constructing(component) print("Constructing", component) end
function Logger.Constructed(component) print("Constructed", component) end
function Logger.Starting(component) print("Starting", component) end
function Logger.Started(component) print("Started", component) end
function Logger.Stopping(component) print("Stopping", component) end
function Logger.Stopped(component) print("Stopped", component) end
local MyComponent = Component.new({Tag = "MyComponent", Extensions = {Logger}})
Sometimes it is useful for an extension to control whether or not a component should be constructed. For instance, if a component on the client should only be instantiated for the local player, an extension might look like this, assuming the instance has an attribute linking it to the player's UserId:
local player = game:GetService("Players").LocalPlayer
local OnlyLocalPlayer = {}
function OnlyLocalPlayer.ShouldConstruct(component)
local ownerId = component.Instance:GetAttribute("OwnerId")
return ownerId == player.UserId
end
local MyComponent = Component.new({Tag = "MyComponent", Extensions = {OnlyLocalPlayer}})
It can also be useful for an extension itself to turn on/off
depending on various contexts. For example, let's take the
Logger from the first example, and only use that extension
if the bound instance has a Log attribute set to true
:
function Logger.ShouldExtend(component)
return component.Instance:GetAttribute("Log") == true
end
In this forked version of component, extensions can also add methods
to the component class and extend other extensions via giving an extension
a Methods
table. For example:
local ExtendedComponentMethods = {}
function ExtendedComponentMethods.DoSomething(component)
print("Hello World!")
end
local MyComponentExtension = {}
MyComponentExtension.Methods = ExtendedComponentMethods
This will add a method called DoSomething
to the component class.
Be careful when using with ShouldExtend
It is important to note that these methods are added to the Component Class
and not the Component Instance
. This means that these methods will be availible
regardless of whether the extension passes its shouldExtend function or not. If
your code is dependent on extension methods existing only when they pass their
shouldExtend function, you may want to avoid using this feature.
If you want to utilize other extensions within your extension or guarantee that the
given extension is loaded onto the component before your extension, you can use
the Extensions
table. For example:
local SomeOtherExtension = require(somewhere.SomeOtherExtension)
local MyComponentExtension = {}
MyComponentExtension.Extensions = {SomeOtherExtension}
This will guarantee that SomeOtherExtension
is added to the component and
loaded before MyComponentExtension
.
info
The ShouldExtend function of SomeOtherExtension
will still be called
independently of the ShouldExtend function of MyExtension
. Under the hood this
just adds the extension to the components original extension array.
ComponentConfig
interface
ComponentConfig {
Tag:
string
--
CollectionService tag to use
}
Component configuration passed to Component.new
.
- If no Ancestors option is included, it defaults to
{workspace, game.Players}
. - If no Extensions option is included, it defaults to a blank table
{}
.
Properties
Started
EventComponent ClassComponent.Started:
Signal
Fired when a new instance of a component is started.
local MyComponent = Component.new({Tag = "MyComponent"})
MyComponent.Started:Connect(function(component) end)
Stopped
EventComponent ClassComponent.Stopped:
Signal
Fired when an instance of a component is stopped.
local MyComponent = Component.new({Tag = "MyComponent"})
MyComponent.Stopped:Connect(function(component) end)
Instance
Component InstanceComponent.Instance:
Instance
A reference back to the Roblox instance from within a component instance. When
a component instance is created, it is bound to a specific Roblox instance, which
will always be present through the Instance
property.
MyComponent.Started:Connect(function(component)
local robloxInstance: Instance = component.Instance
print("Component is bound to " .. robloxInstance:GetFullName())
end)
Functions
new
ComponentCreate a new custom Component class.
local MyComponent = Component.new({Tag = "MyComponent"})
A full example might look like this:
local MyComponent = Component.new({
Tag = "MyComponent",
Ancestors = {workspace},
Extensions = {Logger}, -- See Logger example within the example for the Extension type
})
local AnotherComponent = require(somewhere.AnotherComponent)
-- Optional if UpdateRenderStepped should use BindToRenderStep:
MyComponent.RenderPriority = Enum.RenderPriority.Camera.Value
function MyComponent:Construct()
self.MyData = "Hello"
end
function MyComponent:Start()
local another = self:GetComponent(AnotherComponent)
another:DoSomething()
end
function MyComponent:Stop()
self.MyData = "Goodbye"
end
function MyComponent:HeartbeatUpdate(dt)
end
function MyComponent:SteppedUpdate(dt)
end
function MyComponent:RenderSteppedUpdate(dt)
end
HeartbeatUpdate
Component ClassComponent.
HeartbeatUpdate
(
dt:
number
) →
(
)
If this method is present on a component, then it will be
automatically connected to RunService.Heartbeat
.
Method
This is a method, not a function. This is a limitation of the documentation tool which should be fixed soon.
local MyComponent = Component.new({Tag = "MyComponent"})
function MyComponent:HeartbeatUpdate(dt)
end
SteppedUpdate
Component ClassComponent.
SteppedUpdate
(
dt:
number
) →
(
)
If this method is present on a component, then it will be
automatically connected to RunService.Stepped
.
Method
This is a method, not a function. This is a limitation of the documentation tool which should be fixed soon.
local MyComponent = Component.new({Tag = "MyComponent"})
function MyComponent:SteppedUpdate(dt)
end
RenderSteppedUpdate
This item only works when running on the client. ClientComponent ClassComponent.
RenderSteppedUpdate
(
dt:
number
) →
(
)
If this method is present on a component, then it will be
automatically connected to RunService.RenderStepped
. If
the [Component].RenderPriority
field is found, then the
component will instead use RunService:BindToRenderStep()
to bind the function.
Method
This is a method, not a function. This is a limitation of the documentation tool which should be fixed soon.
-- Example that uses `RunService.RenderStepped` automatically:
local MyComponent = Component.new({Tag = "MyComponent"})
function MyComponent:RenderSteppedUpdate(dt)
end
-- Example that uses `RunService:BindToRenderStep` automatically:
local MyComponent = Component.new({Tag = "MyComponent"})
-- Defining a RenderPriority will force the component to use BindToRenderStep instead
MyComponent.RenderPriority = Enum.RenderPriority.Camera.Value
function MyComponent:RenderSteppedUpdate(dt)
end
GetAll
Component Class
Gets a table array of all existing component objects. For example,
if there was a component class linked to the "MyComponent" tag,
and three Roblox instances in your game had that same tag, then
calling GetAll
would return the three component instances.
local MyComponent = Component.new({Tag = "MyComponent"})
-- ...
local components = MyComponent:GetAll()
for _,component in ipairs(components) do
component:DoSomethingHere()
end
FromInstance
Component Class
Gets an instance of a component class from the given Roblox
instance. Returns nil
if not found.
local MyComponent = require(somewhere.MyComponent)
local myComponentInstance = MyComponent:FromInstance(workspace.SomeInstance)
WaitForInstance
Component ClassResolves a promise once the component instance is present on a given Roblox instance.
An optional timeout
can be provided to reject the promise if it
takes more than timeout
seconds to resolve. If no timeout is
supplied, timeout
defaults to 60 seconds.
local MyComponent = require(somewhere.MyComponent)
MyComponent:WaitForInstance(workspace.SomeInstance):andThen(function(myComponentInstance)
-- Do something with the component class
end)
UpdateAncestors
Component ClassAllows for you to update the valid ancestors of a component class. This is useful if you want to give a valid ancestor that may not exist when the component is first created.
local MyComponent = Component.new({
Tag = "MyComponent",
Ancestors = {workspace},
})
task.defer(function()
local newAncestors = {workspace:WaitForChild("SomeFolder")}
MyComponent:UpdateAncestors(newAncestors)
end)
Construct
Component ClassComponent:
Construct
(
) →
(
)
Construct
is called before the component is started, and should be used
to construct the component instance.
local MyComponent = Component.new({Tag = "MyComponent"})
function MyComponent:Construct()
self.SomeData = 32
self.OtherStuff = "HelloWorld"
end
Start
Component ClassComponent:
Start
(
) →
(
)
Start
is called when the component is started. At this point in time, it
is safe to grab other components also bound to the same instance.
local MyComponent = Component.new({Tag = "MyComponent"})
local AnotherComponent = require(somewhere.AnotherComponent)
function MyComponent:Start()
-- e.g., grab another component:
local another = self:GetComponent(AnotherComponent)
end
Stop
Component ClassComponent:
Stop
(
) →
(
)
Stop
is called when the component is stopped. This occurs either when the
bound instance is removed from one of the whitelisted ancestors or when
the matching tag is removed from the instance. This also means that the
instance might be destroyed, and thus it is not safe to continue using
the bound instance (e.g. self.Instance
) any longer.
This should be used to clean up the component.
local MyComponent = Component.new({Tag = "MyComponent"})
function MyComponent:Stop()
self.SomeStuff:Destroy()
end
GetComponent
Component InstanceRetrieves another component instance bound to the same Roblox instance.
local MyComponent = Component.new({Tag = "MyComponent"})
local AnotherComponent = require(somewhere.AnotherComponent)
function MyComponent:Start()
local another = self:GetComponent(AnotherComponent)
end
ForEachSibling
Component Instance
Ties a function to the lifecycle of the calling component and the equivalent component of the given
componentClass
. The function is run whenever a component of the given class is started. The given
function passes the sibling component of the given class and a janitor to handle any connections
you may make within it. The Janitor is cleaned up whenever either compenent is stopped.
local AnotherComponent = require(somewhere.AnotherComponent)
local MyComponent = Component.new({Tag = "MyComponent"})
function MyComponent:Start()
self:ForEachSibling(AnotherComponent, function(sibling, jani)
print(sibling.SomeProperty)
jani:Add(function()
print("Sibling component stopped")
end)
end)
end