I’m currently working on a UI component system using ModuleScripts. My system is currently laid out like so:
ComponentBase
→ ButtonBase
→ TextButton
→ ContainedButton
.
ComponentBase
is the core of all components. Every component should consist of these variables and functions:
Source Code for ComponentBase
local HTTP = game:GetService 'HttpService'
local Component = {
__GUID = '',
__element = {},
__signal = {},
__var = {},
__state = {}
}
Component.__index = Component
setmetatable(Component, {})
function Component.new()
local this = setmetatable({}, Component)
this.__GUID = HTTP:GenerateGUID(false)
return this
end
function Component:GetGUID()
return self.__GUID
end
return Component
The ButtonBase
class then inherits the ComponentBase
class.
Partial Source Code for ButtonBase
local Util, TextService = script.Parent.Parent.Parent.Utility, game:GetService 'TextService'
local Base, Signal, Create = require(script.Parent.Component), require(Util.Signal), require(Util.Create)
local t, IIF = require(Util.t), require(Util.IIF)
local Component = {}
Component.__index = Component
setmetatable(Component, Base)
function Component.new(Param)
local this = Base.new()
setmetatable(this, Component)
...
this.__state.Disabled = Param.Disabled
this.__element.Root = Create 'ImageButton' {...}
this.__element.Label = Create 'TextLabel' {...}
this.__signal.MouseDown = Signal.new()
this.__signal.MouseUp = Signal.new()
this.__signal.Hovered = Signal.new()
this.__signal.Dragged = Signal.new()
this.__signal.ComponentDisabled = Signal.new()
this.__signal.ComponentEnabled = Signal.new()
this.Event = setmetatable({}, {
__index = function(_, k) return this.__signal[k] end,
__newindex = function() end,
__metatable = getmetatable(script)
})
this.__element.Root.InputBegan:Connect(function(inputObj)
if (this.__state.Disabled) then return end
local Input, E = inputObj.UserInputType, Enum.UserInputType
if (Input == E.MouseButton1 or Input == E.MouseButton2 or Input == E.MouseButton3
or Input == E.Touch or Input == E.Gamepad1 or Input == E.Gamepad2) then
this.__state.Clicked = true
this.__element.Root.Selected = true
this.__signal.MouseDown:Fire(inputObj)
end
if (Input == E.MouseMovement) then
this.__state.Hovered = true
this.__element.Root.Active = true
this.__signal.Hovered:Fire(inputObj)
end
end)
this.__element.Root.InputChanged:Connect(function(inputObj)
if (this.__state.Disabled) then return end
local Input, E = inputObj.UserInputType, Enum.UserInputType
if (Input == E.MouseMovement or Input == E.Touch) then
this.__state.Dragged = true
this.__signal.Dragged:Fire(inputObj)
end
end)
this.__element.Root.InputEnded:Connect(function(inputObj)
if (this.__state.Disabled and not this.__state.Clicked) then return end
local Input, E = inputObj.UserInputType, Enum.UserInputType
if (Input == E.MouseMovement) then
this.__state.Hovered, this.__state.Dragged, this.__state.Clicked = false, false, false
this.__element.Root.Active, this.__element.Root.Selected = false, false
elseif (Input == E.MouseButton1 or Input == E.MouseButton2 or Input == E.MouseButton3
or Input == E.Touch or Input == E.Gamepad1 or Input == E.Gamepad2) then
this.__state.Clicked, this.__state.Dragged = false, false
this.__element.Root.Selected = false
this.__signal.MouseUp:Fire(inputObj)
end
end)
this.Component = this.__element.Root
return this
end
function Component:SetDisabled(Bool)
assert(t.boolean(Bool))
self.__state.Disabled = Bool
self.__element.Root.Selectable = false
self.__signal[Bool and 'ComponentDisabled' or 'ComponentEnabled']:Fire()
end
function Component:SetTextColour(...)
end
function Component:SetLabel(...)
end
function Component:GetLabel()
end
function Component:GetTextSize()
end
function Component:SetParent(...)
end
return Component
TextButton
inherits the ButtonBase
class.
Partial Source for TextButton
local Util = script.Parent.Parent.Utility
local Button, Colour = require(script.Parent.Base.Button), require(script.Parent.Parent.Colour)
local Ripple, t, IIF = require(script.Parent.Ripple), require(Util.t), require(Util.IIF)
local Component = {}
Component.__index = Component
setmetatable(Component, Button)
function Component.new(Param)
local this = Button.new(Param)
setmetatable(this, Component)
this.__var.AutoSize, this.__var.RippleEnabled = Param.AutoSize, Param.RippleEnabled
this.Component.Size = UDim2.new(0, this:GetTextSize().X + 16, 0, 36)
this.__var.ClickRippleConnection = this.Event.MouseDown:Connect(function(inputObj)
if (this.__var.RippleEnabled and not this.__state.Disabled) then
Ripple.new(this.Component, Vector2.new(
inputObj.Position.X - this.Component.AbsolutePosition.X,
inputObj.Position.Y - this.Component.AbsolutePosition.Y
), this.__var.TextColour)
end
end)
this.__element.Label:GetPropertyChangedSignal 'Text':Connect(function()
if (this.__var.AutoSize) then return this:AutoResize() end
end)
this.__var.DisabledStateConnection = this.Event.ComponentDisabled:Connect(function()
this:SetTextColour(Colour.Grey[700], true)
end)
this.__var.EnabledStateConnection = this.Event.ComponentEnabled:Connect(function()
this:SetTextColour(this.__var.TextColour, true)
end)
return this
end
function Component:AutoResize()
end
function Component:SetAutoSizeEnabled(Bool)
end
function Component:GetAutoSizeEnabled()
end
return Component
ContainedButton
inherits TextButton
Partial Source for ContainedButton
local Util = script.Parent.Parent.Utility
local Button, Colour = require(script.Parent.TextButton2), require(script.Parent.Parent.Colour)
local Create, t, IIF, const = require(Util.Create), require(Util.t), require(Util.IIF), require(Util.Const)
local RES = const {
Image = 'rbxassetid://2253325980',
ScaleType = Enum.ScaleType.Slice,
ButtonRect = Vector2.new(20, 20)
}
local Component = {}
Component.__index = Component
setmetatable(Component, Button)
function Component.new(Param)
local this = Button.new(Param)
setmetatable(this, Component)
this.__var.BackgroundColour, this.__var.AutoColourText = Param.BackgroundColour, Param.AutoColourText
this.Component.ZIndex = 2
this.Component.Image, this.Component.ImageRectSize = RES.Image, RES.ButtonRect
this.Component.ImageColor3 = Param.BackgroundColour
if (this.__var.AutoColourText) then this:AutoColourText() end
this.__var.DisabledStateConnection:Disconnect()
this.__var.EnabledStateConnection:Disconnect()
this.__var.DisabledStateConnection = this.Event.ComponentDisabled:Connect(function()
this:SetBackgroundColour(Colour.Grey[700], true)
this:AutoColourText(true)
end)
this.__var.EnabledStateConnection = this.Event.ComponentEnabled:Connect(function()
this:SetBackgroundColour(this.__var.BackgroundColour, true)
this:SetTextColour(this.__var.TextColour, true)
end)
return this
end
function Component:SetBackgroundColour(...)
end
function Component:AutoColourText(...)
end
return Component
However, I am running into problems when :Fire()
ing the signals in my __signal
table. I currently have a LocalScript setup generating several of these buttons in a for
loop. When clicking these buttons, it is only the last-created button that is actually fired; no matter which button I click.
for Shade, Colour in next, Theme do
if (typeof(Colour) == 'Color3') then
local Button = ContainedButton.new {
AutoSize = true,
BackgroundColour = Colour,
TextColour = Colour,
AutoColourText = true
}
Button:SetLabel('Blue: '..tostring(Shade)..' {'..Button:GetGUID()..'}')
Button.Event.MouseDown:Connect(function()
print(Button:GetGUID())
end)
Card:AppendChild(Button.Component)
end
end
There are no errors/warnings in my output. Parameters passed into the .new(...)
statement are reflected accordingly in their respective buttons; Button:SetLabel(...)
sets the label for the button as-expected (with the correct GUID), but the MouseDown
event I have setup always prints the GUID for the last button created; no matter if I click the first, middle, or last.
What am I doing wrong?