DEV Community

Mark Davies
Mark Davies

Posted on

Hackathon update #3 - Building a pager in Blazor

Introduction

You know I feel as though there are a lot of subtle things that happen on websites that people don't appreciate, paging I feel is one of those things, over the years I've just imported whatever was convienient to a project, grabbed a jquery pager or datatables pager because for the most part they are a solved problem and I don't want to deal with it.

But seeing as we've been given an oppertunity as part of the twilio hackathon to create whatever we wanted I wanted to visit what I think is a grossley underviewed component on the page, the pager!

Writing it in c#/blazor (and bootstrap) I'm gonna walk through the code of how I created this wonderful component.

Parameters

So first off, I believe we only need three pieces of information from tha page that is going to include this control

  • Count of items
  • How many per page
  • A callback function

Let's dive in!

We want to create something that looks like this: Pager

Okay, so I'm gonna be a little lazy here and say hey, I already did the work - so for all you people who have read this before or for people who just want something to copy and paste here ya go :D. I hope that some of you stick around to see the explanation.

<div class="container">
    <nav>
        <ul class="pagination justify-content-center">
            <li class="page-item @GetActiveClass(1)">
                <a class="page-link" @onclick="@(async () => await ChangePage(1))">1</a>
            </li>

            @if (_numberOfPages > 1)
            {
                @if (_numberOfPages > 2)
                {
                    @if (_hasLeftSpill)
                    {
                        <li class="page-item">
                            <a class="page-link" aria-label="Previous" @onclick="@(async () => await ChangePage(_currentPage - (NumberOfNeighbours + 1)))">
                                <span aria-hidden="true">
                                    &laquo;
                                </span>
                            </a>
                        </li>
                    }

                    @foreach (var pageId in _pages)
                    {
                        <li class="page-item @GetActiveClass(pageId)">
                            <a class="page-link" @onclick="@(async () => await ChangePage(pageId))">@pageId</a>
                        </li>
                    }

                    @if (_hasRightSpill)
                    {
                        <li class="page-item">
                            <a class="page-link" aria-label="Previous" @onclick="@(async () => await ChangePage(_currentPage + (NumberOfNeighbours + 1)))">
                                <span aria-hidden="true">&raquo;</span>
                            </a>
                        </li>
                    }
                }

                <li class="page-item @GetActiveClass(_numberOfPages)">
                    <a class="page-link" @onclick="@(async () => await ChangePage(_numberOfPages))">@_numberOfPages</a>
                </li>
            }
        </ul>
    </nav>
</div>

@code {
    private int _numberOfPages;
    private int _currentPage = 1;

    private bool _hasLeftSpill;
    private bool _hasRightSpill;
    private List<int> _pages = new List<int>();

    [Parameter]
    public double ItemsPerPage { get; set; }

    [Parameter]
    public double TotalItems { get; set; }

    [Parameter]
    public int NumberOfNeighbours { get; set; }

    [Parameter]
    public Func<int, Task> OnItemClick { get; set; }

    protected override void OnInitialized()
    {
        _numberOfPages = (int)Math.Ceiling(TotalItems / ItemsPerPage);
        ProcessPage();
    }

    private string GetActiveClass(int page) => _currentPage == page ? "active" : string.Empty;

    private void ProcessPage()
    {
        var maxPageNumber = _numberOfPages - 1;
        var maxNeighbours = NumberOfNeighbours * 2;

        var startPage = Math.Max(2, _currentPage - NumberOfNeighbours);
        var neighbourBuffer = _currentPage > NumberOfNeighbours + 1 ? maxNeighbours + 1 : maxNeighbours;

        var endPage = startPage + neighbourBuffer;
        var count = endPage <= maxPageNumber ? neighbourBuffer : _numberOfPages - startPage;

        _pages = Enumerable.Range(startPage, count).ToList();

        _hasLeftSpill = startPage > 2;
        _hasRightSpill = _currentPage < maxPageNumber - NumberOfNeighbours;
    }

    private async Task ChangePage(int pageId)
    {
        pageId = Math.Max(1, pageId);
        pageId = Math.Min(pageId, _numberOfPages);

        await OnItemClick(pageId - 1);
        _currentPage = pageId;

        ProcessPage();
        StateHasChanged();
    }
}

Okay that's a lot of code! but let me take you through things and why this took a good number of hours to really nail down. Let me take you through the code, one function at a time and see where we end up, let's start at the beginning:

protected override void OnInitialized()
{
    _numberOfPages = (int)Math.Ceiling(TotalItems / ItemsPerPage);
    ProcessPage();
}

This is a bit of an easy function to explain, we have a total number of items and a count of items we want to show per page, from these two pieces of information we can find out how many pages we actually need to display all of the data. We use the System.Math library just to say, hey if this is a decimal then just round it up to the nearest number, for some reason this returns a decimal so we just convert it to an int.

We then call a function called ProcessPage which we'll get onto shortly.....

private string GetActiveClass(int page) => _currentPage == page ? "active" : string.Empty;

Another short simple function, this basically take a page id and if the current page is equal to the page id that has been passed in then bobs your uncle we pass back the string "active" which we use to show the user that that particular page is active.

private async Task ChangePage(int pageId)
{
    pageId = Math.Max(1, pageId);
    pageId = Math.Min(pageId, _numberOfPages);

    await OnItemClick(pageId - 1);
    _currentPage = pageId;

    ProcessPage();
    StateHasChanged();
}

pageId = Math.Max(1, pageId);

This function takes a page id, we do a couple of safe guard functions just to be sure that we are operating in normal conditions, we can't possibly be lower than page one so if the page id has somehow been changed to a value less than one then just set it to one.

pageId = Math.Min(pageId, _numberOfPages);

The page id can't possibly be larger than the total number of pages we need to display all of the informaiton presented so we just set it to the total number of pages if it has swayed above that number in any way.

await OnItemClick(pageId - 1);

We call the callback function that the page gave to use to say "hey the user has clciked a page and here is it's id, we reduce it by one here because most (if not all) programming languages start indexing in 0, so this is just to make that a little easier.

_currentPage = pageId;

Set the current page id to the page id we have just specified.

StateHasChanged();

Tell the framwork that something has changed and it most likely needs to re-render something on the page.

private void ProcessPage()
{
    var maxPageNumber = _numberOfPages - 1;
    var maxNeighbours = NumberOfNeighbours * 2;

    var startPage = Math.Max(2, _currentPage - NumberOfNeighbours);
    var neighbourBuffer = _currentPage > NumberOfNeighbours + 1 ? maxNeighbours + 1 : maxNeighbours;

    var endPage = startPage + neighbourBuffer;
    var count = endPage <= maxPageNumber ? neighbourBuffer : _numberOfPages - startPage;

    _pages = Enumerable.Range(startPage, count).ToList();

    _hasLeftSpill = startPage > 2;
    _hasRightSpill = _currentPage < maxPageNumber - NumberOfNeighbours;
}

Can I just say at this moment of time that I hate this funciton, it took me so long to refine it and it to actually make sense and work, this is why people don't write their own pagers. Let's walk through the code:

var maxPageNumber = _numberOfPages - 1;

Ok so.... we print out the last page number manually so in a scenario where we have 10 pages this will be 9 so if you squint and look youll be able to see in your minds eye something like: 1 << 4 | 5 >> 10

var maxNeighbours = NumberOfNeighbours * 2;

We double the amount of neightbours here because we need the same amount of them for each side of our pager.

var startPage = Math.Max(2, _currentPage - NumberOfNeighbours);

Much the same concept here as before we actually print out the first page manually so the minimum number that the pager will start on will be 2, if the current page minus the number of neighbours is more than two then that is where we ned to _start the page count, so for example we were on page 5 of 10 with 2 neighbours we would start at 3.

var neighbourBuffer = _currentPage > NumberOfNeighbours + 1 ? maxNeighbours + 1 : maxNeighbours;

Sigh... this is where things start getting a little complex, so we need a neighbour buffer, so if the user is on a page that is one more than the amount of neighbours then we have to add one to the amount of neighbours (this will be clear soon) for the actual page that we are on so in a two neighbour scenario:

Neighbours: 2,3 | Page we are on: 4 | Neighbours: 5,6

I swear this will make sense when we get to actually creating the list of number, please stick with me....

var endPage = startPage + neighbourBuffer;
var count = endPage <= maxPageNumber ? neighbourBuffer : _numberOfPages - startPage;

End page is basically where the pager will stop counting so if it's going to go above the number of pages that we have then we need to instead count the spaces between the page we are on and the maximum numer of pages.

_pages = Enumerable.Range(startPage, count).ToList();

Thank goodness, so, we are creating a list of number from the number stored in startPage and count are the amount of numbers we want to generate, so in this example: Enumerable.Range(1, 10); we would get this list: 1,2,3,4,5,6,7,8,9,10, yes it is an inclusive call, so if we do Enumerable.Range(2, 10); we will get 2,3,4,5,6,7,8,9,10,11.

I hope that makes sense.

_hasLeftSpill = startPage > 2;

If we are on a page that is more than two (where the pager starts) then we can say that there are pages to the left of us that we are not showing to the user.

_hasRightSpill = _currentPage < maxPageNumber - NumberOfNeighbours;

If we are not on a page that is the amount of neighbours away from the maximum number of pages then we have page number not being printed and have right spill.


What I've learned is that pagers are way harder than I originally thought they were, hopefully you are able to go back to the razor syntax and point at things and make them make sense. Looking forward to getting this website on azure and into the hackathon, I have thought of ways to extend this website and it might actually become a little bit of a pet project of mine but we'll see. Thanks for the opportunity either way, this has been so much fun!


GitHub logo joro550 / Artemis

Volunteer for events in your area

Artemis

Oldest comments (0)