Using jQuery DataTable in Blazor WASM

Viewed 870

I want to integrate jQuery DataTable with my Blazor Wasm project. So I add reference to these files:

//cdn.datatables.net/1.11.4/css/jquery.dataTables.min.css

//cdn.datatables.net/1.11.4/js/jquery.dataTables.min.js

and write this function to call appropriate initial function:

window.ApplyjQueryDatatable = () => {
    $('#example').DataTable();
}

and for create the table I wrote this codes in razor page:

@page "/"
@inject IJSRuntime JsRuntime

<div class="row mt-4 ">
    <div class="col-12">
        <table id="example" @ref="@DataTableContainer">
            <thead>
                <tr>
                    <th>OrderID</th>
                    <th>CustomerID</th>
                    <th>OrderDate</th>
                    <th>ShipCity</th>
                    <th>ShipPostalCode</th>
                    <th>ShipCountry</th>
                </tr>
            </thead>
            <tbody>
                @foreach (var item in OrderPage)
                {
                    <tr>
                        <td>@item.OrderID</td>
                        <td>@item.CustomerID</td>
                        <td>@item.OrderDate</td>
                        <td>@item.ShipCity</td>
                        <td>@item.ShipPostalCode</td>
                        <td>@item.ShipCountry</td>
                    </tr>
                }
            </tbody>
        </table>
    </div>
</div>

@code {
    private ElementReference DataTableContainer;
    private List<Orders> OrderPage { set; get; } = new List<Orders>();

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender == true)
        {
            //Web API Call Simulation

            OrderPage = new List<Orders>();
            OrderPage.Add(new Orders() { CustomerID = "aa1", OrderDate = DateTime.Now, OrderID = 1, ShipCity = "bb1", ShipCountry = "cc1", ShipPostalCode = "dd1" });
            OrderPage.Add(new Orders() { CustomerID = "aa2", OrderDate = DateTime.Now, OrderID = 1, ShipCity = "bb2", ShipCountry = "cc2", ShipPostalCode = "dd2" });
            OrderPage.Add(new Orders() { CustomerID = "aa3", OrderDate = DateTime.Now, OrderID = 1, ShipCity = "bb3", ShipCountry = "cc3", ShipPostalCode = "dd3" });
            OrderPage.Add(new Orders() { CustomerID = "aa4", OrderDate = DateTime.Now, OrderID = 1, ShipCity = "bb4", ShipCountry = "cc4", ShipPostalCode = "dd4" });
            OrderPage.Add(new Orders() { CustomerID = "aa5", OrderDate = DateTime.Now, OrderID = 1, ShipCity = "bb5", ShipCountry = "cc5", ShipPostalCode = "dd5" });

            await JsRuntime.InvokeVoidAsync("ApplyjQueryDatatable");
            StateHasChanged();
        }

        await base.OnAfterRenderAsync(firstRender);
    }
}

After run the project there are 2 problems:

  1. Table created but I got empty table message:

enter image description here

  1. When I click on any columns header table row suddenly disappear and table has empty:

enter image description here

How can I solve these problems?

Thanks


Edit 1)

According to @Henk answer I changed the code this way:

protected override async Task OnInitializedAsync()
{
    var AllData = await apiServices.GetOrders();
    OrderPage = AllData.Take(100).ToList();
    //await JsRuntime.InvokeVoidAsync("ApplyjQueryDatatable");
}

and

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender == true)
    {
        await JsRuntime.InvokeVoidAsync("ApplyjQueryDatatable");
    }
}

and the new problem is when table has created again any of jQuery DataTable features not work. For example if I change the number of record drop down then table will be empty:

enter image description here

and when I click on any of headers:

enter image description here

How can I solve this problem? Thanks


Edit 2)

Again according to @Henk comment: I changed the code this way and it worked:

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender == true)
    {
        var AllData = await apiServices.GetOrders();
        OrderPage = AllData.Take(100).ToList();

        StateHasChanged();
        await JsRuntime.InvokeVoidAsync("ApplyjQueryDatatable");
    }
    base.OnAfterRender(firstRender);
}

I think there is a small point hear. If I change the code this way:

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender == true)
    {
        OrderPage = new List<OrdersDTO>();
        OrderPage.Add(new OrdersDTO() { CustomerID = "aa1", OrderDate = DateTime.Now, OrderID = 1, ShipCity = "bb1", ShipCountry = "cc1", ShipPostalCode = "dd1" });
        OrderPage.Add(new OrdersDTO() { CustomerID = "aa2", OrderDate = DateTime.Now, OrderID = 2, ShipCity = "bb2", ShipCountry = "cc2", ShipPostalCode = "dd2" });
        OrderPage.Add(new OrdersDTO() { CustomerID = "aa3", OrderDate = DateTime.Now, OrderID = 3, ShipCity = "bb3", ShipCountry = "cc3", ShipPostalCode = "dd3" });
        OrderPage.Add(new OrdersDTO() { CustomerID = "aa4", OrderDate = DateTime.Now, OrderID = 4, ShipCity = "bb4", ShipCountry = "cc4", ShipPostalCode = "dd4" });
        OrderPage.Add(new OrdersDTO() { CustomerID = "aa5", OrderDate = DateTime.Now, OrderID = 5, ShipCity = "bb5", ShipCountry = "cc5", ShipPostalCode = "dd5" });
        OrderPage.Add(new OrdersDTO() { CustomerID = "aa6", OrderDate = DateTime.Now, OrderID = 6, ShipCity = "bb6", ShipCountry = "cc6", ShipPostalCode = "dd6" });
        OrderPage.Add(new OrdersDTO() { CustomerID = "aa7", OrderDate = DateTime.Now, OrderID = 7, ShipCity = "bb7", ShipCountry = "cc7", ShipPostalCode = "dd7" });
        OrderPage.Add(new OrdersDTO() { CustomerID = "aa8", OrderDate = DateTime.Now, OrderID = 8, ShipCity = "bb8", ShipCountry = "cc8", ShipPostalCode = "dd8" });
        OrderPage.Add(new OrdersDTO() { CustomerID = "aa9", OrderDate = DateTime.Now, OrderID = 9, ShipCity = "bb9", ShipCountry = "cc9", ShipPostalCode = "dd9" });
        OrderPage.Add(new OrdersDTO() { CustomerID = "aa10", OrderDate = DateTime.Now, OrderID = 6, ShipCity = "bb10", ShipCountry = "cc10", ShipPostalCode = "dd10" });
        OrderPage.Add(new OrdersDTO() { CustomerID = "aa11", OrderDate = DateTime.Now, OrderID = 6, ShipCity = "bb11", ShipCountry = "cc11", ShipPostalCode = "dd11" });
        OrderPage.Add(new OrdersDTO() { CustomerID = "aa12", OrderDate = DateTime.Now, OrderID = 6, ShipCity = "bb12", ShipCountry = "cc12", ShipPostalCode = "dd12" });

        StateHasChanged();
        await JsRuntime.InvokeVoidAsync("ApplyjQueryDatatable");
    }
    base.OnAfterRender(firstRender);
}

if won't work. Straing.......

But if I write it this way it'll work:

await Task.Run(() =>
{
    OrderPage = new List<OrdersDTO>();
    OrderPage.Add(new OrdersDTO() { CustomerID = "aa1", OrderDate = DateTime.Now, OrderID = 1, ShipCity = "bb1", ShipCountry = "cc1", ShipPostalCode = "dd1" });
    OrderPage.Add(new OrdersDTO() { CustomerID = "aa2", OrderDate = DateTime.Now, OrderID = 2, ShipCity = "bb2", ShipCountry = "cc2", ShipPostalCode = "dd2" });
...

Why this code work this way?

2 Answers

Update

With async loading it gets hairier. jQuery will capture the data, that has to happen at exactly the right moment.

bool loaded = false;
protected override async Task OnInitializedAsync()
{
    await LoadData();
    loaded = true;  
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (loaded)
    {
        loaded = false;
        await JsRuntime.InvokeVoidAsync("ApplyjQueryDatatable");
    }        
}

The underlying problem here is that jQuery modifies the DOM and that invalidates the copy that Blazor is holding. So you could get inconsistencies. The better solution is to use a Blazor datagrid.


Old answer

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender == true)
    {
        //Web API Call Simulation
        // Do the async loading here

        await JsRuntime.InvokeVoidAsync("ApplyjQueryDatatable");
        //StateHasChanged();  -- remove
    }
}

I know I am late to this but the solution I currently use in production is to create a component of the table I wish to use for example : <ProductsDataTableComponent productModels="products"</ProductsDataTableComponent>

In this scenario you would add a parameter expecting your table's data in the component and call the AddDataTable js function on initialized and include a dispose methode for DataTablesDispose

@code
{
 [Parameter]
  public List<ProductModel> productModels { get; set; }
  protected override async Task OnInitializedAsync()
  {
    await JS.InvokeVoidAsync("AddDataTable", "#productsTable");
  }

  void IDisposable.Dispose()
  {
    JS.InvokeVoidAsync("DataTablesDispose", "#productsTable");
  }
}

The important part is to wrap your component into an if statement around your component to ensure that it is not rendered before the data is loaded.

So the Final Result would look something Like this:

@if(!(products?.Any() ?? false)) 
{ 
<div class="justify-content-center" style="font-size: 1.2em; text-align: center;">What ever you want to do when the data is empty aka don't render the table</div> 
}
 else 
{ 
<ProductsDataTableComponent productModels="products"</ProductsDataTableComponent> 
}
Related