Components is a simple, streamlined library for implementing highly reusable, flexible custom components for UI, interactive world elements, and more.
What is a component?
Components let you run reusable pieces of code for many different instances in your game at once.
An example of where components can be useful is when defining item pickups in your game. You could write a small piece of code that implements health restoring on touch, then automatically run that code on all your health pickups.
This helps you keep organised as your scripts are all kept in one location, rather than having dozens (or even hundreds) of copies scattered all around your game. By updating the code for a component, you can affect the behaviour of everything that uses it immediately.
Quick demonstration
Hereās our initial setup:
Using Components, weāll create a HelloWorld component, which will turn any UI into a rainbow!
First, weāll create a folder inside our local script called āComponentsā. That folder will contain the code for our components.
Then, in that folder, add a ModuleScript called āHelloWorldā. This will store our componentās implementation, in this case some code to make a UI turn into a rainbow:
Notice the OOP style of the component - when we add the HelloWorld component to something, itāll create a new object for us and call construct()
with the instance it was added to. When itās removed (or when the instance is destroyed) itāll call destroy()
so we can clean up any events we connected to or instances we made.
Now, we just need to write a few lines in our LocalScript. The first line just requires the Components library so we can use it. The second line gives our Components folder to the library, so we can use our HelloWorld component. The third line tells Components to apply our components within ScreenGui.
Now that weāve written our HelloWorld component and written the code to load it in, how do we apply it to something? Itās pretty simple - just add a Configuration instance into it and call it āHelloWorldā.
Because we passed in our ScreenGui on line 3 of our local script, Components will look for any Configuration instances named āHelloWorldā in our ScreenGui. If it finds one, itāll apply our code to the instance itās parented to.
Hereās the end result:
Minimal API design
Components aims to be as simple and streamlined as possible. This makes it easier to learn, and reduces the likelihood of bugs and unintended behaviour.
The library is designed to work similarly to the existing Binder coding pattern. Instead of using CollectionService tags (like Binders do), Configuration instances are used, as they can store values, functions and events, and can be easily added and viewed in the Explorer. This makes using components much easier.
Module summary
The following API summary contains only the most common features - these will be enough for 99% of use cases and workflows. Full documentation, features and explanation can be found through comments in the moduleās source code.
function Components.addClassFolder(folder: Instance)
Finds all descendant ModuleScript instances and adds them as component classes. The module scriptās name will be used as the component name.
function Components.watch(instance: Instance)
Finds all descendant Configuration instances and creates a component for each. The name of the Configuration should be the name of the component to create.
Additionally, this function will listen for new Configuration instances, so you donāt have to manually handle creating components for UI thatās added later.
Component API summary
Module scripts for components should return a function with two arguments; the first will be a reference to the Components module, and the second will be an empty āclassā table:
return function(Components, class)
-- ...
end
Components uses an OOP programming style - a new object is created per component, using the class
table as the template.
Your module must implement the class:construct()
and class:destroy()
methods. If either is missing, your component wonāt work and an error will be raised.
construct()
is called when the object is first initialised. This occurs when your component is added to an instance.
destroy()
is called when the object is about to be destroyed. This occurs when your component is removed from an instance, or when the instance is destroyed.
Make sure you clean up anything you create or connect to inside of destroy()
to avoid memory leaks.
return function(Components, class)
function class:construct(instance: Instance)
print("Hello, " .. instance.Name .. "!")
end
function class:destroy()
print("Goodbye!")
end
end
Result
You can add other methods to class
, just like a regular OOP setup. Additionally, you can access extra properties in these methods:
-
self.instance
- a reference to the instance associated with your component:
-
self.config
- a reference to the Configuration associated with your component:
return function(Components, class)
function class:construct(instance: Instance)
self:extraMethod()
end
function class:extraMethod()
print("self.instance = " .. self.instance.Name)
print("self.config = " .. self.config.Name)
end
function class:destroy()
end
end
Result
Build better, build faster, build with Components
Components is 100% free and open source! Attribution in your game, plugin or whatever is appreciated but not required.
You can add Components to your inventory from the Roblox Library.
Alternatively, you can view the full source code right here, including documentation comments:
Source
--[[
Components
Simpler, smarter, modular components
by Elttob
--]]
local Components = {}
-- A dictionary of component classes, indexed by name.
Components.classes = {}
-- Fired when a component class is added via Components.addClass()
Components.onClassAdded = Instance.new "BindableEvent"
-- Fired when a component class is removed via Components.removeClass()
Components.onClassRemoved = Instance.new "BindableEvent"
-- A map of component configurations to objects, used internally.
local componentConfigs = {}
--[[
Attempts to add the given module script as a component.
The module script must return a function.
That function will be called with two arguments:
- this Components module
- a blank 'class' table
The function should add two methods to the class table:
- a :construct() method
- a :destroy() method
If either are missing, an error will be raised.
The :construct() method will be called when a component of this class
is created for an instance - it will be passed that instance as it's only
argument.
The :destroy() method will be called when a component of this class is
destroyed - either because the instance was destroyed, or because
Components.removeClass() was called for this class.
Other methods can be added to the class table freely if desired. Methods
defined in the class table (including :construct() and :destroy()) can
access some convenient utilities via self:
- self.instance - the instance associated with this component
- self.config - the Configuration instance for this component
Each component must use a unique name - an error will be raised if another
component is already using the same name as this one.
After the component is added successfully, the Components.onClassAdded event
will be fired with one argument - the name of this class.
--]]
function Components.addClass(name: string, module: ModuleScript)
local class = {
construct = nil,
destroy = nil
}
local initFunction = require(module)
if typeof(initFunction) ~= "function" then
error("Module for component " .. name .. " must return a function - see docs for help (at " .. module:GetFullName() .. ")")
end
initFunction(Components, class)
if typeof(class.construct) ~= "function" then
error("Component " .. name .. " is missing the :construct() function (at " .. module:GetFullName() .. ")")
end
if typeof(class.destroy) ~= "function" then
error("Component " .. name .. " is missing the :destroy() function (at " .. module:GetFullName() .. ")")
end
if Components.classes[name] ~= nil then
error("Another component is already named " .. name .. " - names must be unique (at " .. module:GetFullName() .. ")")
end
Components.classes[name] = class
Components.onClassAdded:Fire(name)
end
--[[
Attempts to remove the component with the given name.
An error will be raised if no component was found with the given name.
Just before the component is removed, the Components.onClassRemoved event
will be fired with one argument - the name of this class.
--]]
function Components.removeClass(name: string)
local class = Components.classes[name]
if class == nil then
error("Remove failed as no component called " .. name .. " was found")
end
Components.onClassRemoved:Fire(name)
Components.classes[name] = nil
end
--[[
Convenience function - this finds all descendant module scripts in the given
instance, and attempts to add them as components.
The name of each component will be taken from the name of the module script.
This function uses Components.addClass() internally - any errors raised by
that function won't be caught.
--]]
function Components.addClassFolder(folder: Instance)
for _, descendant in pairs(folder:GetDescendants()) do
if descendant:IsA "ModuleScript" then
Components.addClass(descendant.Name, descendant)
end
end
end
--[[
Returns the component for the specific configuration instance given, or
creates it if the configuration is not yet associated with a component.
If the configuration instance has no parent, an error will be raised - it's
expected that configurations are parented to instances to avoid issues
with destruction detection.
The class will be derived from the name of the configuration - if no class
is found with that name, an error will be raised.
If the configuration is moved out of the current instance, or if the
component class is removed, then the component will automatically be
destroyed.
--]]
function Components.getComponent(config: Configuration)
if componentConfigs[config] then
return componentConfigs[config]
end
if config.Parent == nil then
error("Can't create a component for a configuration with no parent (at " .. config:GetFullName() .. ")")
end
local class = Components.classes[config.Name]
if class == nil then
error("Can't create component named " .. config.Name .. " as no class was found (at " .. config:GetFullName() .. ")")
end
local object = setmetatable({}, {__index = class})
object.instance = config.Parent
object.config = config
object:construct(object.instance)
componentConfigs[config] = object
local unparentConnection
local classRemoveConnection
unparentConnection = config.AncestryChanged:Connect(function()
if config.Parent ~= object.instance then
object:destroy()
unparentConnection:Disconnect()
classRemoveConnection:Disconnect()
componentConfigs[config] = nil
end
end)
classRemoveConnection = Components.onClassRemoved.Event:Connect(function(className)
if className == config.Name then
object:destroy()
unparentConnection:Disconnect()
classRemoveConnection:Disconnect()
componentConfigs[config] = nil
end
end)
return object
end
--[[
Iterates through the descendants of the given instance, creating components
for any configurations it finds (that don't already have a component).
This function then listens for new descendants being added - if a
configuration is added as a descendant later, a component will still be
created (again, only if a component wasn't already created earlier).
If a new component class is added later, the descendants will be iterated
over again to check for any components of that class type which would have
been missed earlier.
To be more specific about how components are detected, whenever this
function encounters a Configuration instance whose name matches a
component name, it'll call Components.getComponent() on that
configuration instance. All errors are accounted for, so no errors should
be raised by this function during normal usage.
There's a non-obvious behaviour of this function as implemented currently.
Due to an odd engine limitation (read: no Destroyed event) this function
will stop listening to events if the given instance is parented to nil.
This avoids memory leaks by cleaning up connections after instances are
destroyed, but means your code should avoid parenting the instance to nil.
Note that components created by this function have their own lifecycle -
if an instance with a component is moved elsewhere, the component will
continue to exist. Components are only destroyed when their configuration
is unparented, which typically happens when the instance is destroyed.
Furthermore, this means components, unlike the instance passed to this
function, *can* be safely parented to nil. Keep these notes in mind if you
manipulate component lifecycles unconventionally.
It's useful to apply this to containers such as ScreenGuis.
However, be cognizant of possible performance implications of using this on
large containers - it's not recommended to use on the whole data model, for
instance.
--]]
function Components.watch(instance: Instance)
for _, descendant in pairs(instance:GetDescendants()) do
if descendant:IsA "Configuration"
and Components.classes[descendant.Name] ~= nil
then
Components.getComponent(descendant)
end
end
local descendantAddConnection
local classAddConnection
local destroyConnection
descendantAddConnection = instance.DescendantAdded:Connect(function(descendant)
if descendant:IsA "Configuration"
and Components.classes[descendant.Name] ~= nil
then
Components.getComponent(descendant)
end
end)
classAddConnection = Components.onClassAdded.Event:Connect(function(className)
for _, descendant in pairs(instance:GetDescendants()) do
if descendant:IsA "Configuration"
and descendant.Name == className
then
Components.getComponent(descendant)
end
end
end)
--FUTURE: replace this with Destroyed if/when that's implemented
destroyConnection = instance.AncestryChanged:Connect(function()
if instance.Parent == nil then
descendantAddConnection:Disconnect()
classAddConnection:Disconnect()
destroyConnection:Disconnect()
end
end)
return descendantAddConnection
end
return Components
Hope this helps! If it did, you might also like my beautifully utilitarian plugins:
Reclass ā¢ Imiji ā¢ Atmos ā¢ Pick ā¢ InCommand