How to make a good UI module?

Hello, I’m not much of a frontend developer, but over the years I’ve learned a few things to make decentish code, but I’d like to get some help on improving more. My go to approach for UI is just looping through the PlayerGui and setting up button events on anything that’s a button and then using a open and close menu function pretty simple. The only issue is I feel like it’s a bit messy to work with since it requires each button or frame to have a module to handle the button logic.

function buttonAction(button,action)
	local frame = button:FindFirstAncestorOfClass("Frame") 

	if frame:FindFirstChild("DISABLED") then
		return
	end

	if script.parent:FindFirstChild(frame.Name) then
		local module = require(script.Parent[frame.Name])
		if action == "pressed" and module[button.Name] then
			module[button.Name](button,frame)
		elseif action == "enter" and module[button.Name.."Enter"] then 
			module[button.Name.."Enter"](button,frame)	
		elseif action == "leave" and module[button.Name.."Leave"] then 
			module[button.Name.."Leave"](button,frame)		
		end
	else
		warn("No module with name",frame.Name)
	end	
end

local function connectEventsToButton(button)
	if not button then return end
	
	button.MouseEnter:Connect(function()
		buttonAction(button,"enter")
	end)

	button.MouseLeave:Connect(function()
		buttonAction(button,"leave")
	end)

	button.MouseButton1Click:Connect(function()
		buttonAction(button,"pressed")
	end)	
end

local function setupButtons()
	for _,button in ipairs(playerGui:GetDescendants()) do
		if button:IsA("TextButton") or button:IsA("ImageButton") then
			connectEventsToButton(button)	
		end
	end	

	screenGui.DescendantAdded:Connect(function(child)
		if child:IsA("TextButton") or child:IsA("ImageButton") then
			connectEventsToButton(child)
		end
	end)
end

-- Not sure what to call this yet, but makes sure that the main menu is visible
-- and nothing else.
local function test()
	game.Lighting.MainMenuBlur.Enabled = true
	mainMenuGui.Enabled = true
	screenGui.Enabled = false
	
	for _,frame in pairs(screenGui:GetChildren()) do
		if frame:IsA("Frame") then
			frame.Visible = false
		end
	end
end

function UI:openMenu(menuID)
	local newMenu = screenGui[menus[menuID]]

	if not newMenu then
		warn("Invalid menu ID:", menuID)
		return
	end
	
	if #menuStack > 0 then
		-- Hide the previous menu
		menuStack[#menuStack].Visible = false
	end
	
	-- Add the new menu to the stack and show it
	table.insert(menuStack, newMenu)
	newMenu.Visible = true
end

function UI:closeMenu()
	if #menuStack == 0 then return end

	-- Hide the current menu
	menuStack[#menuStack].Visible = false
	table.remove(menuStack)
end

Screenshot 2023-02-23 212043

1 Like

You dont need to create a module per button, you could split the buttons per system/gui or even place all the function in only one module:

Example:

local UImodule = require(script.Parent:WaitForChild("UI"))

function buttonAction(button,action)
	local frame = button:FindFirstAncestorOfClass("Frame") 

	if frame:FindFirstChild("DISABLED") then
		return
	end

	if UImodule[button.Name] then
		UImodule[button.Name][action](button, frame)
	else
		warn("No function in module for", button.Name)
	end	
end

Module:

local UI = {}

--[[ PLAY FUNCTIONS ]]
UI.Play = {}
UI.Play["pressed"] = function(btn, frame)
	warn(btn, frame)
end
UI.Play["enter"] = function(btn, frame)
	warn(btn, frame)
end
UI.Play["leave"] = function(btn, frame)
	warn(btn, frame)
end

--[[ SETTINGS FUNCTIONS ]]
UI.Settings = {}

--[[ STORE FUNCTIONS]]
UI.Store = {}

return UI

Still similar, but you would need only one module per system or just one module holding all the functions in GUI

This was actually what I used to do, however, there are a few issues I ran into. The first one was the module just got way to big and pretty fast depending on the amount of buttons. The second was I can’t have buttons with the same name so if I have multiple frames that have a close or back button I’d have to put everything into 1 function

My suggestion would be just put a LocalScript right below the ScreenGUI (Same hierarchy as the frame) then add logic individually. This is a good approach because you can’t tell how long the code is going to be for one button. It is better to put the LocalScript in there to prevent you to find 1 single function among 100 of functions that you may create later.

And because you methodology is making one script that handle all of the UI, this approach get messy super fast. I do realize that you habe search feature by hitting Ctrl+F and you can search whatever function you want, but what if you forget the name of the function? Putting a new script inside the UI can save much more time in case that your game is big.

1 Like