DEV Community

Cover image for Building a Custom Server-Side DataTable in Laravel Without Packages
Hoang Manh Cam
Hoang Manh Cam

Posted on

Building a Custom Server-Side DataTable in Laravel Without Packages

When dealing with large datasets in Laravel applications, client-side rendering can quickly become inefficient. In this article, I'll demonstrate how to implement server-side DataTables processing from scratch without relying on packages like Yajra Laravel DataTables.

Why Build a Custom Solution?

While packages provide convenience, building your own implementation offers:

  • Complete control over the data processing pipeline
  • Better understanding of the underlying mechanics
  • No dependencies on third-party packages
  • Customized to your specific application needs

Project Architecture

Our implementation consists of:

  1. Backend Service - Handles data processing and filtering
  2. Controller - Processes AJAX requests
  3. View - Contains the DataTable HTML structure
  4. JavaScript - Configures and initializes the DataTable

Step 1: Create a DataTable Service

First, let's create a dedicated service to handle server-side processing:

<?php

namespace App\Services;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Schema;

class DataTableService
{
    public function processDataTable($query, Request $request, $columns): array
    {
        // Get the table name from the query
        $tableName = $query->getModel()->getTable();

        // Handle search
        if ($request->has('search') && !empty($request->input('search')['value'])) {
            $searchValue = $request->input('search')['value'];
            $query->where(function ($query) use ($searchValue, $columns, $tableName) {
                foreach ($columns as $column) {
                    // Check if the column belongs to a related table
                    if (str_contains($column, '.')) {
                        // Split the column name into relation and column
                        [$relation, $relatedColumn] = explode('.', $column);

                        // Add a condition on the related table
                        $query->orWhereHas($relation, function ($query) use ($relatedColumn, $searchValue) {
                            $query->where($relatedColumn, 'like', '%'.$searchValue.'%');
                        });
                    } else {
                        // Skip columns that don't exist in the table
                        if (Schema::hasColumn($tableName, $column)) {
                            $query->orWhere($column, 'like', '%'.$searchValue.'%');
                        }
                    }
                }
            });
        }

        // Handle ordering
        if ($request->has('order')) {
            $order = $request->input('order')[0];
            $orderByColumn = $columns[$order['column']];
            $orderDirection = $order['dir'];

            if (Schema::hasColumn($tableName, $orderByColumn)) {
                $query->orderBy($orderByColumn, $orderDirection);
            }
        }

        // Get total count before pagination
        $totalFiltered = $query->count();

        // Handle pagination
        $start = $request->input('start', 0);
        $length = $request->input('length', 10);
        $query->skip($start)->take($length);

        // Get filtered count
        $totalData = $query->count();

        // Prepare response data
        return [
            'draw' => intval($request->input('draw')),
            'recordsTotal' => $totalData,
            'recordsFiltered' => $totalFiltered,
            'data' => $query->get(),
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

This service handles core functionality including:

  • Searching across columns
  • Relation-based searching
  • Sorting data
  • Implementing pagination
  • Formatting the response for DataTables

Step 2: Implement the Controller

Next, create a controller method to process DataTable requests:

public function index(Request $request)
{
    if ($request->ajax()) {
        $columns = ['id', 'family_name', 'first_name', 'email', 'created_at', 'id'];
        $data = User::select($columns);

        $response = $this->dataTableService->processDataTable($data, $request, $columns);
        // Add URLs and format fields for each record
        $response['data'] = $response['data']->map(function ($user) {
            $user->edit_url = route('admin.users.edit', $user->id);
            $user->destroy_url = route('admin.users.destroy', $user->id);
            $user->email = '<a href="mailto:' . $user->email . '">' . $user->email . '</a>';
            return $user;
        });

        return response()->json($response);
    }

    return view('admin.users.index');
}
Enter fullscreen mode Exit fullscreen mode

The controller:

  1. Checks if the request is an AJAX call
  2. Sets up the columns to query
  3. Passes the query builder to our service
  4. Enhances the response with additional data (URLs, formatted content)
  5. Returns either JSON for AJAX requests or the view for normal requests

Step 3: Create the Blade View with Inline JavaScript

Now let's create a complete Blade view with the DataTable initialization code included directly:

@extends('admin.layouts.master')
@section('title')
    Users
@endsection
@section('page-header')
    @component('admin.components.page-header')
        @slot('title')
            Users List
        @endslot
        @slot('subtitle')
            Users
        @endslot
        @slot('button')
            <div class="my-auto ms-auto">
                <a href="{{ route('admin.users.create') }}" class="btn btn-info">
                    Create
                </a>
            </div>
        @endslot
    @endcomponent
@endsection

@section('center-scripts')
    <script src="https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js"></script>
    <script src="https://cdn.datatables.net/buttons/2.3.6/js/dataTables.buttons.min.js"></script>
@endsection

@section('content')
    <div class="content">
        <div class="card">
            <table class="table datatable-selection-single" id="user-table">
                <thead>
                <tr>
                    <th>ID</th>
                    <th>Last Name</th>
                    <th>First Name</th>
                    <th>Email</th>
                    <th>Created At</th>
                    <th class="text-center">Actions</th>
                </tr>
                </thead>
                <tbody>
                </tbody>
            </table>
        </div>
    </div>
@endsection

@section('scripts')
<script>
    // DataTable language configuration
    let dataTableLanguage = {
        'loadingRecords': 'Loading...',
        'searchPlaceholder': 'Search...',
        'lengthMenu': '_MENU_',
        'paginate': {
            'first': 'First',
            'last': 'Last',
            'next': '&rarr;',
            'previous': '&larr;'
        },
        'sLengthMenu': "Display _MENU_ items",
        'oPaginate': {
            'sNext': "Next",
            'sPrevious': "Previous"
        },
        'sInfo': "Displaying _START_ to _END_ of _TOTAL_ items.",
        'sSearch': '_INPUT_',
        'sZeroRecords': "No data to display",
        'sInfoEmpty': "Displaying 0 of 0 items",
        'sInfoFiltered': "Filter from all _MAX_ items"
    };

    $(document).ready(function () {
        // Initialize DataTable directly without a helper function
        $('#user-table').DataTable({
            retrieve: true,
            processing: true,
            serverSide: true,
            ajax: {
                url: "{{ route('admin.users.index') }}",
                type: "GET"
            },
            columns: [
                {'data': 'id'},
                {'data': 'family_name'},
                {'data': 'first_name'},
                {'data': 'email'},
                {'data': 'created_at'},
                {
                    data: null,
                    sortable: false,
                    render: function (data) {
                        return `<div class="d-inline-flex w-100 justify-content-center">
                            <a href="${data.edit_url}"><i class="ph-note-pencil me-2"></i></a>
                            <form method="POST" action="${data.destroy_url}" class="delete-form" data-id="${data.id}">
                              @csrf
                              @method('DELETE')
                              <button type="submit" class="dropdown-item delete-button text-danger">
                                  <i class="ph-trash"></i>
                              </button>
                             </form>
                         </div>`;
                    },
                },
            ],
            dom: '<"datatable-header justify-content-start"f<"ms-sm-auto"l><"ms-sm-3"B>><"datatable-scroll"t><"datatable-footer"ip>',
            language: dataTableLanguage
        });

        // Delete confirmation handler
        $(document).on('click', '.delete-button', function (event) {
            event.preventDefault();
            const form = $(this).closest('form');

            // Example of a simple delete confirmation
            if (confirm('Are you sure you want to delete this user?')) {
                form.submit();
            }
        });
    });
</script>
@endsection

Enter fullscreen mode Exit fullscreen mode

Advantages of This Approach

  1. No External Dependencies: The implementation doesn't rely on packages like Yajra Laravel DataTables
  2. Self-Contained Templates: All JavaScript is included directly in the Blade template
  3. Easier Debugging: Having the code in one place makes it easier to understand and debug
  4. Performance Optimization: Server-side processing only fetches the data needed for each request
  5. Customizable: You have complete control over the implementation

Error Handling

To add error handling, you can modify the AJAX call to include error callbacks:

// Inside the DataTable initialization
ajax: {
    url: "{{ route('admin.users.index') }}",
    type: "GET",
    error: function (xhr, error, thrown) {
        console.error('DataTable error:', error);
        alert('An error occurred while loading data. Please try again.');
    }
}
Enter fullscreen mode Exit fullscreen mode

Demo

Check out the working demo here:

Conclusion

Building a custom server-side DataTable implementation gives you complete control over your data processing pipeline. This approach is particularly useful for applications with specific requirements or those that need to minimize dependencies.

By merging all JavaScript code directly into the Blade template, we've created a self-contained solution that's easier to maintain and debug. The backend service ensures efficient data processing, while the controller handles application-specific logic.

This solution can be extended to handle more complex scenarios like relation searches, custom filtering, or specific data formatting needs.

Top comments (0)