Incorrect Signals Fired in Inherited Classes

I’m currently working on a UI component system using ModuleScripts. My system is currently laid out like so:
ComponentBaseButtonBaseTextButtonContainedButton.

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?

2 Likes

You’re storing object state in the metatable. It should be stored in the instance. That is,

function Component.new()
	local this = setmetatable({--[[STATE GOES HERE]]}, Component)
	...
end
3 Likes

Such a small, silly mistake; but fixed everything. Cheers!

2 Likes