Is there a Pages:AdvanceToNextPageAsync equivalent for going back a page?

I’m currently making a player leaderboard where a player can click through pages of names. I already made a scrolling leaderboard before, but I feel like it didn’t fit right with the style of the game.

I’ve looked around the API and found Pages:AdvanceToNextPageAsync(), which I thought would be easy to implement. But, there wasn’t any function to go back to the previous page. Am I missing something, or completely misunderstood what it does? Any help would be apreciated!

Leaderboard script (which currently just displays first ten keys, gui button events aren't present either)
local function updateLeaderboard(datastore,leaderboard,connectEvent)
	local surfaceGui =  leaderboard.SurfaceGui
	local frame = surfaceGui.Leaderboard
	local UIGridLayout = frame.UIGridLayout
	
	local leftPageArrow = surfaceGui.LeftPageArrow
	local rightPageArrow = surfaceGui.RightPageArrow

	local success, pages = pcall(function()
		return datastore:GetSortedAsync(false,10)
	end)
	if success then
		if pages then
			local function getPage()
				local currentPage = pages:GetCurrentPage()

				for _,v in ipairs(frame:GetChildren()) do
					if v:IsA("Frame") then
						v:Destroy()
					end
				end
				
				for i,data in ipairs(currentPage) do
					local username
					pcall(function()
						username = Players:GetNameFromUserIdAsync(string.sub(data["key"],8,string.len(data["key"])))
					end)
					if username then
						username = string.upper(username)
						if data["value"] > 0 then
							local leaderboardRankClone = leaderboardRank:Clone()
							leaderboardRankClone.Parent = frame

							local rank = leaderboardRankClone:WaitForChild("Rank")
							local usernameGui = leaderboardRankClone:WaitForChild("Username")
							local value = leaderboardRankClone:WaitForChild("Value")

							local denominations = {"ST", "ND", "RD", "TH"}
							local denomination 
							if i == 1 then
								denomination = denominations[1]
							elseif i % 10 == 2 then
								denomination = denominations[2]
							elseif i % 10 == 3 then
								denomination = denominations[3]
							else
								denomination = denominations[4]
							end

							local textColor
							if i == 1 then
								textColor = Color3.fromRGB(255, 255, 0)
							elseif i == 2 then
								textColor = Color3.fromRGB(192, 192, 192)
							elseif i == 3 then
								textColor = Color3.fromRGB(205, 127, 50)
							else
								textColor = Color3.fromRGB(255, 255, 255)
							end

							rank.TextColor3 = textColor
							usernameGui.TextColor3 = textColor
							value.TextColor3 = textColor

							rank.Text = i .. denomination
							usernameGui.Text = username
							value.Text = data["value"]
						end
					end
				end
			end			
		end
	end
end

Here is what I’m planning to do. There will probably be an issue where players try to change pages at the same time, but there are only 4 players in a server so eh.

RobloxScreenShot20210204_195830697

2 Likes

Can’t you simply cache the pages? It’s probably a lot more lighter on resources that way.

6 Likes

Oh why didn’t I think of that? Thank you for pointing that out!

1 Like

Hm, I don’t quite know how to get the key and value of each page since it’s its own object, and then a table. I tried yanking some code in the DevHub, but I don’t really understand what it exactly does.

Code
local function updateLeaderboard(datastore,leaderboard,connectEvent)
	local surfaceGui =  leaderboard.SurfaceGui
	local frame = surfaceGui.Leaderboard
	local UIGridLayout = frame.UIGridLayout
	
	local page = surfaceGui.Page
	local leftPageArrow = surfaceGui.LeftPageArrow
	local rightPageArrow = surfaceGui.RightPageArrow
	
	local pageIndex = leaderboard.PageIndex

	local success, pages = pcall(function()
		return datastore:GetSortedAsync(false,10)
	end)
	if success then
		if pages then
			local function iterPageItems(pages)
				return coroutine.wrap(function()
					local pagenum = 1
					while true do
						for _, item in ipairs(pages:GetCurrentPage()) do
							coroutine.yield(item, pagenum)
						end
						if pages.IsFinished then
							break
						end
						pages:AdvanceToNextPageAsync()
						pagenum += 1
					end
				end)
			end
			
			for item, pageNo in iterPageItems(pages) do
				print(item, " ", pageNo)
				pagesCache[pageNo] = item
			end
			local function getPage(page,pageIndex)
				if pageIndex.Value > MAX_PAGES then
					pageIndex.Value = 1
				elseif pageIndex.Value < 1 then
					pageIndex.Value = MAX_PAGES
				end

				local currentPage = page[pageIndex.Value]
				
				for key,value in pairs(page) do
					for k,val in pairs(value) do
						print(k, val)
					end
				end

				for _,v in ipairs(frame:GetChildren()) do
					if v:IsA("Frame") then
						v:Destroy()
					end
				end

				for i,data in ipairs(currentPage) do
					local username
					pcall(function()
						username = Players:GetNameFromUserIdAsync(string.sub(data["key"],8,string.len(data["key"])))
					end)
					if username then
						username = string.upper(username)
						if data["value"] > 0 then
							local leaderboardRankClone = leaderboardRank:Clone()
							leaderboardRankClone.Parent = frame

							local rank = leaderboardRankClone:WaitForChild("Rank")
							local usernameGui = leaderboardRankClone:WaitForChild("Username")
							local value = leaderboardRankClone:WaitForChild("Value")

							local denomination = getDenomination(i)

							local rankColor = getRankColor(i)

							rank.TextColor3 = rankColor
							usernameGui.TextColor3 = rankColor
							value.TextColor3 = rankColor

							rank.Text = i .. denomination
							usernameGui.Text = username
							value.Text = data["value"]

							page.Text = pageIndex .. " / " .. MAX_PAGES
						end
					end
				end
			end		
			
			getPage(pagesCache,pageIndex)

			if connectEvent then
				print("connect")
				leftPageArrow.MouseButton1Click:Connect(function()
					print("left")
					pageIndex -= 1
					getPage(pagesCache,pageIndex)
				end)

				rightPageArrow.MouseButton1Click:Connect(function()
					print("right")
					pageIndex += 1
					getPage(pagesCache,pageIndex)
				end)
			end
		end
	end
end
Output

output

2 Likes

Well you gotta check what’s in those tables. The output window inside studio allows you to check what is inside those tables.

2 Likes

It’d probably be best for you to break a Pages object into a dictionary if you want to freely iterate back and forth between pages. Each key of your dictionary would be the page number and the value would be the key-value pairs fetched from the DataStore.

local dataDictionary = {}

local success, pages = pcall(function ()
    return datastore:GetSortedAsync(false, 10)
end)

if success and pages then
    local pageNumber = 1

    while not pages.IsFinished do
        local pageCache = {}
        dataDictionary[pageNumber] = pageCache

        for _, dataPair in ipairs(pages:GetCurrentPage()) do
            table.insert(pageCache, dataPair)
        end

        if not pages.IsFinished then
            pages:AdvanceToNextPageAsync()
            pageNumber += 1
        end
    end
end

Something along these lines.

You know, that being said though, with the advent of UIPageLayout I think it would be much more intuitive for you to use this as your way of paginating instead because this actually allows you to flip back and forth between “pages”. This way, your leaderboard and all its pages are immediately written out and changing pages is just a matter of a simple visual update handled by the Gui engine.

Expected flow:

  1. For the first time in the server or every time the leaderboards are to be updated, use GetSortedAsync to query data. Only use GetSortedAsync during an auto update or for the first time since the server started.

  2. Use a UIPageLayout on your leaderboard. Give it whatever properties you want. For SortOrder, make sure the sorting mode is LayoutOrder.

  3. For every page that exists, create a new frame and make its LayoutOrder is the number of the current page you’re going through. Populate the frame as you go with the entries of players. You’re ahead of the game if you’re using UIListLayout with LayoutOrder SortOrder - each player entry’s LayoutOrder should be the key’s value.

  4. You’re done working with DataStores. Now every time a player clicks the forward or back arrow, use the Next or Previous functions accordingly.

3 Likes

Ok, I finally got around to doing this, but I’m having a hard time trying to use UIPageLayout and pages in general.

I used your code for iterating through pages but I realized the while loop never executes anything because my ordered datastore only has 1 page, so pages.IsFinished returns true. I can’t find another way to loop through them.

You’re ahead of the game if you’re using UIListLayout with LayoutOrder SortOrder - each player entry’s LayoutOrder should be the key’s value.

Does UIListLayout replace UIGridLayout? Why would I need a sort order when the datastore is already sorted for us? I think I’m confusing too many things right now hehe

Code
local function updateLeaderboard(datastore,leaderboard)
	local surfaceGui =  leaderboard.SurfaceGui
	local frame = surfaceGui.Leaderboard
	local UIPageLayout = frame.UIPageLayout
	
	local pageText = surfaceGui.Page
	local leftPageArrow = surfaceGui.LeftPageArrow
	local rightPageArrow = surfaceGui.RightPageArrow

	local success, pages = pcall(function()
		return datastore:GetSortedAsync(false,10)
	end)
	
	if success and pages then
		local pageNumber = 1
		while not pages.IsFinished do
			local pageClone = pageFrame:Clone()
			pageClone.Parent = frame
			pageClone.LayoutOrder = pageNumber
			
			for key,value in pairs(pages:GetCurrentPage()) do
				local username
				pcall(function()
					username = Players:GetNameFromUserIdAsync(string.sub(key,8,string.len(key)))
				end)
				if username then
					username = string.upper(username)
					if value > 0 then
						local leaderboardRankClone = leaderboardRank:Clone()
						leaderboardRankClone.Parent = pageClone

						local rank = leaderboardRankClone:WaitForChild("Rank")
						local usernameGui = leaderboardRankClone:WaitForChild("Username")
						local value = leaderboardRankClone:WaitForChild("Value")

						local denomination = getDenomination(1)

						local rankColor = getRankColor(1)

						rank.TextColor3 = rankColor
						usernameGui.TextColor3 = rankColor
						value.TextColor3 = rankColor

						rank.Text = denomination
						usernameGui.Text = username
						value.Text = value

						--page.Text = .. " / " .. MAX_PAGES
					end
				end
			end
			
			if not pages.IsFinished then
				pages:AdvanceToNextPageAsync()
				pageNumber += 1
			end
		end
	end
end
1 Like

UIPageLayout is intended to display elements in a paged form as the name suggests. What you want to look to be doing is setting up page frames where in each of those frames contains all the leaderboard data of the current DataStore page.

If the while loop doesn’t execute anything, my bad, I tried to shorthand and cut some corners without doing some testing. There usually will always be at minimum one page available even if the page is empty, so a normal while true loop would be good enough to pack the DataStore pages into.

New loop:

while true do
    local pageCache = {}
    dataDictionary[pageNumber] = pageCache

    for _, entry in ipairs(pages:GetCurrentPage()) do
        table.insert(pageCache, entry)
    end

    if pages.IsFinished then
        break
    end

    pages:AdvanceToNextPageAsync()
    pageNumber += 1
end

UIListLayout and UIGridLayout cannot both be used at the same time, but I’m under the assumption that you will be using UIListLayout since you don’t exactly have a grid view going on and you can pack everything into lines worth of content as well. As for SortOrder, that’s to force the layouts to display in their intended order because the sorted data has no effect on how the Gui elements are sorted.

1 Like

Yesss I got the leaderboard to work with UIPageLayout and UIListLayout! Thank you so much for helping me!

leaderboard

But, I have this one small problem where the gui buttons won’t fire when I click on them. Should they be located in a different script, because I currently have them connected in the same script where I create the leaderboard.

Code
local function updateLeaderboard(data,leaderboard)
	local surfaceGui =  leaderboard.SurfaceGui
	local frame = surfaceGui.Leaderboard
	local UIPageLayout = frame.UIPageLayout

	local pageText = surfaceGui.Page
	local leftPageArrow = surfaceGui.LeftPageArrow
	local rightPageArrow = surfaceGui.RightPageArrow

	local pageIndex = surfaceGui.Page
	
	local pageNumber = 1
	local numOfPages = #data
	for _,page in ipairs(data) do
		local pageClone = pageFrame:Clone()
		pageClone.Parent = frame
		pageClone.LayoutOrder = pageNumber

		for i,data in pairs(page) do
			local username
			pcall(function()
				username = Players:GetNameFromUserIdAsync(string.sub(data["key"],8,string.len(data["key"])))
			end)
			if username then
				username = string.upper(username)
				if data["value"] > 0 then
					local leaderboardRankClone = leaderboardRank:Clone()
					leaderboardRankClone.Parent = pageClone
					leaderboardRankClone.LayoutOrder = -data["value"]

					local rank = leaderboardRankClone:WaitForChild("Rank")
					local usernameGui = leaderboardRankClone:WaitForChild("Username")
					local value = leaderboardRankClone:WaitForChild("Value")

					local denomination = getDenomination(i)

					local rankColor = getRankColor(i)

					rank.TextColor3 = rankColor
					usernameGui.TextColor3 = rankColor
					value.TextColor3 = rankColor

					rank.Text = i .. denomination
					usernameGui.Text = username
					value.Text = data["value"]

					pageIndex.Text = pageNumber .. "/" .. numOfPages
				end
			end
		end
		leftPageArrow.Activated:Connect(function()
			print("left")
			UIPageLayout:Next()
		end)

		rightPageArrow.Activated:Connect(function()
			print("right")
			UIPageLayout:Previous()
		end)
	end
end

It probably has to do with SurfaceGuis only catching input when the SurfaceGui is in PlayerGui. You can either have the server send leaderboard data to the client so they can organise it themselves or move the page controls to a separate SurfaceGui. The latter option should really only be used in case of trying to make the leaderboard work quickly for release; the former option (RemoteEvents) would be the option for doing it properly.

1 Like