Goffo.BlazorDataTable 0.3.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Goffo.BlazorDataTable --version 0.3.0
NuGet\Install-Package Goffo.BlazorDataTable -Version 0.3.0
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Goffo.BlazorDataTable" Version="0.3.0" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Goffo.BlazorDataTable --version 0.3.0
#r "nuget: Goffo.BlazorDataTable, 0.3.0"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Goffo.BlazorDataTable as a Cake Addin
#addin nuget:?package=Goffo.BlazorDataTable&version=0.3.0

// Install Goffo.BlazorDataTable as a Cake Tool
#tool nuget:?package=Goffo.BlazorDataTable&version=0.3.0

Blazor DataTable

This is library with Blazor Components that allow the setup of a table with the ability of pagination, sorting and filtering.

Setup

Add to your head tag

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
 <script src="https://kit.fontawesome.com/ce3dedc60a.js" crossorigin="anonymous"></script>

Add at the end of your body

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>

Add to program.cs

using BlazorDataTable;
builder.Services.AddBlazorDataTable();

Example Razor

@page "/"
@inject IHttpClientFactory _httpClientFactory
@inject ILogger<Index> _logger
@inject NavigationManager _navigationManager

<PageTitle>Index</PageTitle>

@if (_isLoading)
{
    <p>Loading...</p>
}
else
{
    <div class="my-3">
        <button class="btn-primary" @onclick="GoToNewPage">Go To New Page</button>
    </div>

    <DataTable TItem="PersonModel"
               Items="_people"
               CssClass="table table-hover table-bordered table-striped table-responsive"
               UsePagination="true"
               PaginationRecordsPerPageOptions="new List<int> { 5, 10, 20, 100, 500 }"               
               PaginationRecordsPerPage="_recordsPerPage"
               PaginationAddAllRecordsOption="false"
               PaginationCurrentPage="_currentPage"
               PaginationTotalRecords="GetTotalRecords()"
               RememberSortAndFilters="true"
               PaginationOnRecordsPerPageChanged="RecordsPerPageChanged"
               PaginationOnSelectedPage="PageSelectionChanged"
               OnSortChanged="SortChanged">
        <DataColumnHeaders>
            <DataColumnHeader ColumnHeaderText="ID" ColumnName="Id" DefaultSortDirection="SortDirection.Disabled"></DataColumnHeader>
            <DataColumnHeader ColumnHeaderText="First Name" ColumnName="FirstName" DefaultSortDirection="SortDirection.None"></DataColumnHeader>
            <DataColumnHeader ColumnHeaderText="Last Name" ColumnName="LastName" DefaultSortDirection="SortDirection.None"></DataColumnHeader>
            <DataColumnHeader ColumnHeaderText="Age" ColumnName="Age" DefaultSortDirection="SortDirection.None"></DataColumnHeader>
            <DataColumnHeader ColumnHeaderText="Email Address" ColumnName="EmailAddress" DefaultSortDirection="SortDirection.None"></DataColumnHeader>
        </DataColumnHeaders>
        <Data>
            <DataColumns OnFilterValueChanged="FilterValueChanged"
                         OnClearFilter="FilterCleared"
                         OnFilterTypeChanged="FilterTypeChanged"
                         OnRangeMinValueChanged="MinRangeValueChanged"
                         OnRangeMaxValueChanged="MaxRangeValueChanged"
                         >
                <DataColumnFilters>
                    <DataColumnFilter SearchIsDisabled=true DisabledSearchIsVisible=true></DataColumnFilter>
                    <DataColumnFilter ColumnName="FirstName" AllowNullOrEmptySearch=false DefaultFilterType="FilterType.NullOrEmpty"></DataColumnFilter>
                    <DataColumnFilter ColumnName="LastName"></DataColumnFilter>
                    <DataColumnFilter ColumnName="Age" UseRangeFilter="true" MinRangeValue="_minRangeAgeValue" MaxRangeValue="_maxRangeAgeValue" MinValue="_minAgeValue" MaxValue="_maxAgeValue"></DataColumnFilter>
                    <DataColumnFilter ColumnName="EmailAddress"></DataColumnFilter>
                </DataColumnFilters>
                <DataDisplayColumns>
                    <TextDataColumn>@context.Id</TextDataColumn>
                    <TextDataColumn>@context.FirstName</TextDataColumn>
                    <TextDataColumn>@context.LastName</TextDataColumn>
                    <TextDataColumn>@context.Age</TextDataColumn>
                    <TextDataColumn>@context.Email</TextDataColumn>
                </DataDisplayColumns>
            </DataColumns>
        </Data>
    </DataTable>

Example Code Behind

    #region Properties and Fields

    private HttpClient _httpClient = new();
    private List<PersonModel> _people = new();
    private string _baseAddress = string.Empty;
    private string? _sqlWhere;
    private string? _sqlOrderBy;
    private Dictionary<string, Dictionary<string, SqlFilter>> _filters = new();
    private Dictionary<string, SqlSort> _sorts = new();
    private IDictionary<string, string?> _queryParameters = new Dictionary<string, string?>();
    private string? _apiQueryString = string.Empty;
    private int _recordsPerPage = 7; //Defaulted to 10 records per page
    private int _currentPage = 1; //Default to start at page 1
    private int _totalRecords;
    private int _minRangeAgeValue;
    private int _maxRangeAgeValue;
    private int _minAgeValue;
    private int _maxAgeValue;
    private bool _isLoading = true;

    #endregion

    #region Overrides

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            _httpClient = _httpClientFactory.CreateClient("LocalClient");
            _baseAddress = _httpClient.BaseAddress?.ToString()!;

            //Sets the Initial Data
            await SetMinRangeAgeValue();
            await SetMaxRangeAgeValue();
            await SetPeople();
            ToggleIsLoading();
            StateHasChanged();
        }

        await base.OnAfterRenderAsync(firstRender);
    }

    #endregion

    #region For DataTable

    private int GetTotalRecords()
    {
        _totalRecords = 0;

        if (_people.Any())
        {
            _totalRecords = _people.First().TotalRecords;
        }

        return _totalRecords;
    }

    private async Task RecordsPerPageChanged(int recordsPerPage)
    {
        //For when the Records per page is changed
        if(_recordsPerPage == recordsPerPage)
        {
            return;
        }

        _recordsPerPage = recordsPerPage;
        _currentPage = 1; //I want to have it reset back to first page

        //Update data
        await SetPeople();
    }

    private async Task PageSelectionChanged(int page)
    {
        //For when the page is changed
        if(_currentPage == page)
        {
            return;
        }

        _currentPage = page;

        //Update data
        await SetPeople();
    }

    private async Task SortChanged(SortTableSearchEventArgs args)
    {
        //This is where you have your logic for sorting when the Sort Icon gets clicked (Order:  None, Ascending, Descending)

        if(string.IsNullOrWhiteSpace(args.ColumnName))
        {
            _logger.LogInformation("{0} was null for {1} method", nameof(args.ColumnName), nameof(SortChanged));
            return;
        }

        //This is just bringing back the sorted current page of data
        //It may be different for you
        var sortValues = args.SortedColumns;

        if (sortValues.ContainsKey(args.ColumnName))
        {
            sortValues[args.ColumnName] = args.SortDirection;
        }
        else
        {
            sortValues.Add(args.ColumnName, args.SortDirection);
        }

        //Updates the data
        SetSqlOrderBy(sortValues);
        await SetPeople();
    }

    private async Task FilterValueChanged(FilterTableSearchEventArgs args)
    {
        //This is where you handle filtering based on the text that is entered into the column search
        if(string.IsNullOrWhiteSpace(args.ColumnName) || args.ColumnName == "Age")
        {
            return;
        }

        //This is just bringing back the filtered current page of data
        //It may be different for you
        try
        {
            var searchValues = args.ColumnSearchValues;

            if (searchValues.ContainsKey(args.ColumnName))
            {
                searchValues[args.ColumnName].SearchValue = args.SearchValue;
            }
            else
            {
                searchValues.Add(args.ColumnName, new ColumnFilter(args.FilterType, args.SearchValue));
            }

            //Updates the data
            SetSqlWhere(searchValues);
            await SetPeople();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, ex.Message);
        }
    }

    private async Task FilterCleared(ClearTableSearchEventArgs args)
    {
        //This is where you handle filtering based on the text that is entered into the column search is cleared out
        //It may be different for you

        //Updates the data
        SetSqlWhere(args.ColumnSearchValues);
        SetSqlOrderBy(args.SortedColumns);
        await SetPeople();
    }

    private async Task FilterTypeChanged(FilterTypeEventArgs args)
    {
        //This is where you handle filtering based on the Filter Type Icon being clicked and changed (Order: Starts With, Contains, Ends With)
        //It may be different for you


        //This is just bringing back the filtered current page of data
        //It may be different for you
        try
        {
            var filters = args.ColumnFilters;

            if (filters.ContainsKey(args.ColumnName))
            {
                filters[args.ColumnName].FilterType = args.Filter.FilterType;
            }
            else
            {
                filters.Add(args.ColumnName, new ColumnFilter(args.Filter.FilterType, args.Filter.SearchValue));
            }

            //Updates data
            SetSqlWhere(filters);
            await SetPeople();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, ex.Message);
        }
    }

    private void MinRangeValueChanged(MinRangeEventArgs args)
    {
        //Here is where you handle when the Min Value changes

        _minAgeValue = args.MinRangeValue;
        _logger.LogInformation("{0} has been changed to {1}", nameof(_minAgeValue), _minAgeValue);
    }

    private async Task MaxRangeValueChanged(MaxRangeEventArgs args)
    {
        //Here is where you handle when the Max Value changes

        _maxAgeValue = args.MaxRangeValue;
        _logger.LogInformation("{0} has been changed to {1}", nameof(_maxAgeValue), _maxAgeValue);
        await Task.CompletedTask;
        StateHasChanged();
    }

    private async Task SetMinRangeAgeValue()
    {
        _minRangeAgeValue = int.MinValue;
        _apiQueryString = @$"{_baseAddress}GetMinAge";

        try
        {
            var output = await _httpClient.GetFromJsonAsync<List<MinAgeModel>>(_apiQueryString);

            if (output is not null)
            {
                if (output.Count == 1)
                {
                    _minRangeAgeValue = output.Single().MinAge;
                    _logger.LogInformation("{0} has been set to {1}", nameof(_minRangeAgeValue), _minRangeAgeValue);
                }
                else
                {
                    _logger.LogError("Multiple records were returned for MinAgeValue");
                    return;
                }
            }
            else
            {
                _logger.LogError("Output for MinAgeValue was null");
                return;
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, ex.Message);
            return;
        }
    }

    private async Task SetMaxRangeAgeValue()
    {
        _maxRangeAgeValue = 0;
        _apiQueryString = @$"{_baseAddress}GetMaxAge";

        try
        {
            var output = await _httpClient.GetFromJsonAsync<List<MaxAgeModel>>(_apiQueryString);

            if (output is not null)
            {
                if (output.Count == 1)
                {
                    _maxRangeAgeValue = output.Single().MaxAge;
                    _logger.LogInformation("{0} has been set to {1}", nameof(_maxRangeAgeValue), _maxRangeAgeValue);
                }
                else
                {
                    _logger.LogError("Multiple records were returned for MaxAgeValue");
                    return;
                }
            }
            else
            {
                _logger.LogError("Output for MaxAgeValue was null");
                return;
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, ex.Message);
            return;
        }
    }

    #endregion

    #region Get Data (Not required for DataTable but would be how you are getting your data)

    private void SetApiQueryString()
    {
        _queryParameters.Clear();
        _queryParameters.Add("startPage", _currentPage.ToString());
        _queryParameters.Add("recordsPerPage", _recordsPerPage.ToString());

        if (!string.IsNullOrWhiteSpace(_sqlWhere))
        {
            _queryParameters.Add("sqlWhere", _sqlWhere);
        }

        if (!string.IsNullOrWhiteSpace(_sqlOrderBy))
        {
            _queryParameters.Add("sqlOrderBy", _sqlOrderBy);
        }

        _apiQueryString = QueryHelpers.AddQueryString(_baseAddress + "GetPeopleDynamic", _queryParameters);
    }

    private async Task SetPeople()
    {
        SetApiQueryString();

        try
        {
            var people = await _httpClient.GetFromJsonAsync<List<PersonModel>>(_apiQueryString);

            if (people is not null)
            {
                _people.Clear();

                if (people.Any())
                {
                    _people.AddRange(people);
                    _logger.LogInformation("{0} = {1}", nameof(_people), _people.Count);
                    _totalRecords = _people.First().TotalRecords;
                    _logger.LogInformation("{0} = {1}", nameof(_totalRecords), _totalRecords);
                }
                else
                {
                    _logger.LogInformation("No records returned.");
                }
            }
            else
            {
                _logger.LogWarning("{0} was null", nameof(people));
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, ex.Message);
        }
    }

    #endregion

    #region Additional Methods (Not required for DataTable)

    private void GoToNewPage()
    {
        _navigationManager.NavigateTo("/NewPage");   
    }

    private void ToggleIsLoading()
    {
        _isLoading = !_isLoading;
    }

    private void SetSqlOrderBy(Dictionary<string, SortDirection> sortedColumns)
    {
        //This is for the api call to get data
        _sorts.Clear();
        SqlSort? sqlSort = null;
        _sqlOrderBy = null;

        foreach (var column in sortedColumns)
        {
            sqlSort = null;

            if (column.Value == SortDirection.Ascending)
            {
                sqlSort = SqlSort.ASC;
            }
            else if (column.Value == SortDirection.Descending)
            {
                sqlSort = SqlSort.DESC;
            }

            if (sqlSort.HasValue)
            {
                _sorts.Add(column.Key, sqlSort.Value);
            }
        }

        if (_sorts.Count > 0)
        {
            _sqlOrderBy = SqlBuilder.BuildOrderByClause(_sorts);
        }
    }

    private void SetSqlWhere(Dictionary<string, ColumnFilter> columnSearchValues)
    {
        //This is for the api call to get the data
        _sqlWhere = null;
        _filters.Clear();
        SqlFilter sqlFilter = SqlFilter.StartsWith;
        FilterType columnFilter = FilterType.StartsWith;

        foreach (var column in columnSearchValues)
        {
            columnFilter = column.Value.FilterType;

            if (columnFilter == FilterType.StartsWith)
            {
                sqlFilter = SqlFilter.StartsWith;
            }
            else if (columnFilter == FilterType.Contains)
            {
                sqlFilter = SqlFilter.Contains;
            }
            else if (columnFilter == FilterType.EndsWith)
            {
                sqlFilter = SqlFilter.EndsWith;
            }
            else if (columnFilter == FilterType.NullOrEmpty)
            {
                sqlFilter = SqlFilter.NullOrEmpty;
            }
            else
            {
                sqlFilter = SqlFilter.StartsWith;
            }

            if (!string.IsNullOrEmpty(column.Value.SearchValue))
            {
                _filters.Add(column.Key, new Dictionary<string, SqlFilter>() { { column.Value.SearchValue!, sqlFilter } });
            }
            else
            {
                if(sqlFilter == SqlFilter.NullOrEmpty)
                {
                    _filters.Add(column.Key, new Dictionary<string, SqlFilter>() { { sqlFilter.ToString(), sqlFilter } });
                }
            }
        }

        if (_filters.Count > 0)
        {
            _sqlWhere = SqlBuilder.BuildWhereClause(_filters);
        }
    }

    #endregion

Other Information

  1. DataColumnHeader

    1. ColumnHeaderText is the Column Header Text to display in the table
    2. DefaultSortDirection is defaulted to None
      1. Other options are Hidden, Disabled, Ascending and Descending
    3. ColumnName comes back in the OnSortChanged Event Args
  2. DataColumnFilter

    1. ColumName comes back in the Event args for Events in the DataColumns razor component
  3. There are also different DataColumn components that have callback methods to peform your event task

    1. TextDataColumn - text
    2. ButtonDataColumn - button
    3. CheckBoxDataColumn - checkbox
    4. SelectDataColumn - dropdown list
    5. DataDisplayColumn - Put anything you want in it (One or more of the above or your own html)
Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
0.8.0 107 1/31/2024
0.7.0 91 1/19/2024
0.6.0 90 1/17/2024
0.5.0 78 1/17/2024
0.4.0 84 1/17/2024
0.3.0 209 10/19/2023
0.2.0 123 9/15/2023
0.1.0 132 9/14/2023

Added option in DataColumnFilter to allow for Searching Null or Empty values.  Added an option to display text if no data is found.