DEV Community

Imam Ali Mustofa
Imam Ali Mustofa

Posted on • Edited on • Originally published at darkterminal.prose.sh

1 Endpoint Do 5 Things for HTMX DataTable

Aaaaaaaaaaaaaaahhh... (sorry, I just wake up!). Hey! What's goin punk! Did you still playing the hates song while you writing code? Btw, get some coffee and sit down please. Let's talk about what I've done this week! This is soooooooooo wonderful!

I just did TursoSyncd and doing freestyle with my fck PHP Framework and HTMX. Yes! HTMX with X, I've done writing my own DataTables (like) using PHP + HTMX + MySQL.

Building table and especially a DataTable is insanely annoying for me and myself! But I love doing some boring stuff and take more time to mess up and walk in to the dead zone (to looking myself as a Handsome and stupid person in the earth).

Is that interactive? Like React Table by TanStack?

Yes, but not a fully features like TanStack have. This is my personal preferences. So hate me please...

So what kinda stuff you have?

Hold on, hold on... lemmee show you...

The HTMX DataTable

<!-- Filename: fck-htmx/views/components/dashboard/partials/customers.approval/table.php -->
<div class="overflow-x-auto" id="customers-table">
    <div class="flex absolute justify-center items-center -mt-2 -ml-2 w-full h-full rounded-lg bg-zinc-400 bg-opacity-35 -z-10 htmx-indicator" id="table-indicator"><?= Icons::use('HxIndicator', 'w-24 h-24') ?></div>
    <div class="flex flex-row justify-between mb-3">
        <h2 class="card-title">Customer Approval</h2>
        <input
            type="search"
            name="search"
            placeholder="Search here..."
            id="search"
            value="<?= $customers['search'] ?>"
            class="w-80 input input-sm input-bordered focus:outline-none"
            hx-get="<?= base_url('customers?' . http_build_query(array_merge($currentPage, ['column' => 'customers.customerId']))) ?>"
            hx-trigger="input changed delay:500ms, search"
            hx-swap="outerHTML"
            hx-target="#customers-table"
            hx-indicator="#table-indicator"
            autocomplete="off"
        />
    </div>
    <table class="table table-zebra">
        <thead>
            <tr>
                <th <?= buildHxAttributes('customers', $customers['queryString'], 'customers.customerId', $customers['activeColumn'], 'customers-table') ?>>#</th>
                <th <?= buildHxAttributes('customers', $customers['queryString'], 'customers.customerName', $customers['activeColumn'], 'customers-table') ?>>Customer Name</th>
                <th <?= buildHxAttributes('customers', $customers['queryString'], 'profiles.firstName', $customers['activeColumn'], 'customers-table') ?>>Marketing Executive</th>
                <th <?= buildHxAttributes('customers', $customers['queryString'], 'customers.customerStoreName', $customers['activeColumn'], 'customers-table') ?>>Store Name</th>
                <th <?= buildHxAttributes('customers', $customers['queryString'], 'customers.customerAddress', $customers['activeColumn'], 'customers-table') ?>>Address</th>
                <th class="cursor-not-allowed">Status</th>
                <th class="cursor-not-allowed">Distance</th>
            </tr>
        </thead>
        <tbody>
            <?php foreach ($customers['data'] as $customer) : ?>
                <tr>
                    <th>C#<?= $customer->customerId ?></th>
                    <td><?= $customer->customerName ?></td>
                    <td><?= $customer->firstName . ' ' . $customer->lastName ?></td>
                    <td><?= $customer->customerStoreName ?></td>
                    <td><?= $customer->customerAddress ?></td>
                    <td><?= $customer->isActived ? 'Active' : 'Inactive' ?></td>
                    <td>
                        <?php
                        $customerSource = explode(',', $customer->customerCoordinate);
                        $appSource = explode(',', '-7.7682121,110.4083341');
                        echo round(Utils::haversine(
                            [
                                'lat' => $customerSource[0],
                                'long' => $customerSource[1],
                            ],
                            [
                                'lat' => $appSource[0],
                                'long' => $appSource[1],
                            ]
                        )) . " Km";
                        ?>
                    </td>
                </tr>
            <?php endforeach; ?>
        </tbody>
    </table>
    <div class="flex flex-row justify-between mt-3">
        <p>
            Page <span class="font-bold"><?= $customers['currentPage'] ?></span> from <span class="font-bold"><?= $customers['totalPages'] ?></span> Total <span class="font-bold"><?= $customers['totalRows'] ?></span> |
            Jump to: <input type="number" name="pageNumber" id="pageNumber" hx-on:change="var url = '<?= base_url('customers?' . http_build_query(array_merge($prevPage, ['column' => 'customers.customerId', 'search' => '']))) ?>';
                var replacedUrl = url.replace(/page=\d+/, 'page=' + this.value);
                htmx.ajax('GET', replacedUrl, {target: '#customers-table', swap: 'outerHTML'})" class="w-12 input input-sm input-bordered" min="1" max="<?= $customers['totalPages'] ?>" value="<?= $customers['currentPage'] ?>" hx-indicator="#table-indicator" />
            Display: <select
                class="w-48 select select-bordered select-sm"
                hx-indicator="#table-indicator"
                hx-on:change="var url = '<?= base_url('customers?' . http_build_query(array_merge($prevPage, ['column' => 'customers.customerId']))) ?>'
                    var pageNumber = parseInt('<?= $prevPage['page'] ?>') == 0 ? 1 : parseInt('<?= $prevPage['page'] ?>')
                    var replacedUrl = url.replace(/limit=\d+/, 'limit=' + this.value);
                    htmx.ajax('GET', replacedUrl.replace(/page=\d+/, 'page=' + pageNumber), {target: '#customers-table', swap: 'outerHTML'})
                ">
                <option <?= $customers['limit'] == 10 ? 'selected' : '' ?> value="10">10 Rows</option>
                <option <?= $customers['limit'] == 20 ? 'selected' : '' ?> value="20">20 Rows</option>
                <option <?= $customers['limit'] == 30 ? 'selected' : '' ?> value="30">30 Rows</option>
                <option <?= $customers['limit'] == 40 ? 'selected' : '' ?> value="40">40 Rows</option>
                <option <?= $customers['limit'] == 50 ? 'selected' : '' ?> value="50">50 Rows</option>
            </select>
        </p>
        <div class="join">
            <button class="join-item btn btn-sm" <?= hxPagination('customers', http_build_query(array_merge($prevPage, ['column' => 'customers.customerId'])), 'customers-table') ?> <?= ($customers['currentPage'] <= 1) ? 'disabled' : '' ?>>«</button>
            <button class="join-item btn btn-sm">Page <?= $customers['currentPage'] ?></button>
            <button class="join-item btn btn-sm" <?= hxPagination('customers', http_build_query(array_merge($nextPage, ['column' => 'customers.customerId'])), 'customers-table') ?> <?= $customers['currentPage'] >= $customers['totalPages'] ? 'disabled' : '' ?>>»</button>
        </div>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

😲 The f is that?!! Are you kidding me??!

🤣 No... but I mean yes! But... That's only the HTML Stucture with HTMX stuff and PHP Code. I will break down it to you. Please give me more loud volume for your hated song...

Markup #1: The Search Field

The Search Field

<input
    type="search"
    name="search"
    placeholder="Search here..."
    id="search"
    value="<?= $customers['search'] ?>"
    class="w-80 input input-sm input-bordered focus:outline-none"
    hx-get="<?= base_url('customers?' . http_build_query(array_merge($currentPage, ['column' => 'customers.customerId']))) ?>"
    hx-trigger="input changed delay:500ms, search"
    hx-swap="outerHTML"
    hx-target="#customers-table"
    hx-indicator="#table-indicator"
    autocomplete="off"
/>
Enter fullscreen mode Exit fullscreen mode

What you see? Yes, the f hx- attribute!

  • hx-get to issues a GET to the specified URL
  • hx-swap to controls how content will swap in (outerHTML, beforeend, afterend, …)
  • hx-target to specifies the target element to be swapped
  • hx-indicator the element to put the htmx-request class on during the request

Read: HTMX Reference

That's the spirit of this freestyle in my messy code! Is this interesting? Just said "yessssss...." for me!

Markup #2: The th Section

<tr>
    <th <?= buildHxAttributes('customers', $customers['queryString'], 'customers.customerId', $customers['activeColumn'], 'customers-table') ?>>#</th>
    <th <?= buildHxAttributes('customers', $customers['queryString'], 'customers.customerName', $customers['activeColumn'], 'customers-table') ?>>Customer Name</th>
    <th <?= buildHxAttributes('customers', $customers['queryString'], 'profiles.firstName', $customers['activeColumn'], 'customers-table') ?>>Marketing Executive</th>
    <th <?= buildHxAttributes('customers', $customers['queryString'], 'customers.customerStoreName', $customers['activeColumn'], 'customers-table') ?>>Store Name</th>
    <th <?= buildHxAttributes('customers', $customers['queryString'], 'customers.customerAddress', $customers['activeColumn'], 'customers-table') ?>>Address</th>
    <th class="cursor-not-allowed">Status</th>
    <th class="cursor-not-allowed">Distance</th>
</tr>
Enter fullscreen mode Exit fullscreen mode

😕 Dude! Come on... what the punk is that buildHxAttributes?

That's PHP Function to generate htmx attribute that I need to sorting-order the data by column. Here is the code look like...

function buildHxAttributes($base_url, $queryString, $column, $activeColumn, $targetId, $search = '', $extraClasses = '')
{
    $hxAttributes = 'hx-get="' . base_url($base_url . '?' . $queryString . '&column=' . $column . '&search=' . $search) . '" ';
    $hxAttributes .= 'hx-swap="outerHTML" ';
    $hxAttributes .= 'hx-target="#' . $targetId . '" ';
    $hxAttributes .= 'hx-indicator="#table-indicator" ';
    $hxAttributes .= 'class="cursor-pointer ';
    if ($activeColumn === $column) {
        $hxAttributes .= 'bg-base-300 ';
    }
    $hxAttributes .= $extraClasses . '"';

    return $hxAttributes;
}
Enter fullscreen mode Exit fullscreen mode

😲 Ooooooooooohhh...

Can I...

😲 Yes please...

Markup #3: The Trampoline

Jump to

Jump to: 
  <input 
     type="number"
     name="pageNumber"
     id="pageNumber"
     hx-on:change="var url = '<?= base_url('customers?' . http_build_query(array_merge($prevPage, ['column' => 'customers.customerId', 'search' => '']))) ?>';
                var replacedUrl = url.replace(/page=\d+/, 'page=' + this.value);
                htmx.ajax('GET', replacedUrl, {target: '#customers-table', swap: 'outerHTML'})"
    class="w-12 input input-sm input-bordered"
    min="1"
    max="<?= $customers['totalPages'] ?>"
    value="<?= $customers['currentPage'] ?>"
    hx-indicator="#table-indicator"
  />
Enter fullscreen mode Exit fullscreen mode

Did you just write PHP and JavaScript inside the attribute like this, inline?

hx-on:change="
  var url = '<?= base_url('customers?' . http_build_query(array_merge($prevPage, ['column' => 'customers.customerId', 'search' => '']))) ?>';
  var replacedUrl = url.replace(/page=\d+/, 'page=' + this.value);
  htmx.ajax('GET', replacedUrl, {target: '#customers-table', swap: 'outerHTML'})
"
Enter fullscreen mode Exit fullscreen mode

Yes... that's call "use client & server";, sorry I am joking! Please give me_ 👏 👏 👏

Markup #4: Display Limit

Display Limit

This is the select options field to allow user choose limit data that displayed in the table. Hmmmm... sound promising! 🤣

Display: <select
    class="w-48 select select-bordered select-sm"
    hx-indicator="#table-indicator"
    hx-on:change="var url = '<?= base_url('customers?' . http_build_query(array_merge($prevPage, ['column' => 'customers.customerId']))) ?>'
        var pageNumber = parseInt('<?= $prevPage['page'] ?>') == 0 ? 1 : parseInt('<?= $prevPage['page'] ?>')
        var replacedUrl = url.replace(/limit=\d+/, 'limit=' + this.value);
        htmx.ajax('GET', replacedUrl.replace(/page=\d+/, 'page=' + pageNumber), {target: '#customers-table', swap: 'outerHTML'})
    ">
    <option <?= $customers['limit'] == 10 ? 'selected' : '' ?> value="10">10 Rows</option>
    <option <?= $customers['limit'] == 20 ? 'selected' : '' ?> value="20">20 Rows</option>
    <option <?= $customers['limit'] == 30 ? 'selected' : '' ?> value="30">30 Rows</option>
    <option <?= $customers['limit'] == 40 ? 'selected' : '' ?> value="40">40 Rows</option>
    <option <?= $customers['limit'] == 50 ? 'selected' : '' ?> value="50">50 Rows</option>
</select>
Enter fullscreen mode Exit fullscreen mode

Aaaaaaaaaaand again! JavaScript code inside the attribute value 😄 this is funny right!?

Markup #5: The Pagination

Pagination

<button class="join-item btn btn-sm" <?= hxPagination('customers', http_build_query(array_merge($prevPage, ['column' => 'customers.customerId'])), 'customers-table') ?> <?= ($customers['currentPage'] <= 1) ? 'disabled' : '' ?>>«</button>
<button class="join-item btn btn-sm">Page <?= $customers['currentPage'] ?></button>
<button class="join-item btn btn-sm" <?= hxPagination('customers', http_build_query(array_merge($nextPage, ['column' => 'customers.customerId'])), 'customers-table') ?> <?= $customers['currentPage'] >= $customers['totalPages'] ? 'disabled' : '' ?>>»</button>
Enter fullscreen mode Exit fullscreen mode

This markup will display group buttons left display prev icon and middle the current page and next icon. Also I create a PHP Function called hxPagination just for fun!

function hxPagination($base_url, $queryString, $targetId)
{
    $hxAttributes = 'hx-get="' . base_url($base_url . '?' . $queryString) . '" ';
    $hxAttributes .= 'hx-swap="outerHTML" ';
    $hxAttributes .= 'hx-target="#' . $targetId . '" ';
    $hxAttributes .= 'hx-indicator="#table-indicator" ';

    return $hxAttributes;
}
Enter fullscreen mode Exit fullscreen mode

Eeeeh.... nothing fancy here! Of course... it's just markup! Wawawaiiiit... special markup with "x" 😄


Everything is defined and setup (maybe) correctly! Now I need to create a router, controller, and the model

The Router

<?php
// Filename: fck-htmx/routes/web.php

use Fckin\core\Application;

/** @var Application $app  */

$app->router->get('/', 'Customers@datatable');
Enter fullscreen mode Exit fullscreen mode

Router done!

The Controller

<?php
// Filename: fck-htmx/controllers/Customers.php

namespace App\controllers;

use App\config\helpers\Utils;
use App\models\Customers as ModelsCustomers;
use Fckin\core\Controller;
use Fckin\core\Request;
use Fckin\core\Response;

class Customers extends Controller
{
    protected $customers;

    public function __construct()
    {
        $response = new Response();
        if (!isAuthenticate()) {
            $response->setStatusCode(401);
            exit();
        }
        $this->customers = new ModelsCustomers();
    }

    public function datatable(Request $request)
    {
        $customers = $this->customers->notActiveDataTables(
            $request->getQuery('column'),
            \strtoupper($request->getQuery('order')),
            $request->getQuery('page'),
            $request->getQuery('limit'),
            $request->getQuery('search')
        );
        $direction = $request->getQuery('order') === 'asc' ? 0 : 1;
        $params = [
            'customers' => [
                ...$customers,
                'search' => $request->getQuery('search'),
                'activeColumn' => $request->getQuery('column'),
                'order' => $direction,
                'page' => (int) $request->getQuery('page'),
                'limit' => (int) $request->getQuery('limit'),
                'queryString' => \http_build_query([
                    'order' => $direction === 0 ? 'desc' : 'asc',
                    'page' => $request->getQuery('page'),
                    'limit' => $request->getQuery('limit')
                ])
            ]
        ];
        return Utils::addComponent('dashboard/partials/customers.approval/table', $params);
    }
}
Enter fullscreen mode Exit fullscreen mode

Controller is done!

The Model

<?php
// Filename: fck-htmx/models/Customers.php

namespace App\models;

use Fckin\core\db\Model;
use PDO;

class Customers extends Model
{
    public function notActiveDataTables(string $column = 'customers.customerId', string $order_by = 'ASC', int $page = 1, int $limit = 10, string $search = ''): array
    {
        $totalRows = $this->executeQuery('customers_all_is_inactive')->rowCount();

        $limit = empty($search) && ($limit === 10 || $limit === $totalRows) ? 10 : $limit;

        $offset = ($page - 1) * $limit;

        $sql = "SELECT
            customers.*,
            users.*,
            roles.*,
            profiles.*
        FROM
            customers
        JOIN users ON customers.userId = users.userId
        JOIN roles ON users.roleId = roles.roleId
        JOIN profiles ON profiles
            .userId = users.userId
        WHERE
            customers.isActived = 0 AND (
                customers.customerName LIKE '%{$search}%' OR profiles.firstName LIKE '%{$search}%' OR profiles.lastName LIKE '%{$search}%'
            )
        ORDER BY
            {$column} {$order_by}
        LIMIT {$offset}, {$limit}";
        $results = $this->query($sql);

        $totalPages = ceil($totalRows / $limit);

        return [
            'currentPage' => $page,
            'nextPage' => min($page + 1, $totalPages),
            'totalPages' => $totalPages,
            'offset' => $offset,
            'totalRows' => $totalRows,
            'data' => $results->fetchAll(PDO::FETCH_OBJ)
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

Model is done!


That's it! Here the result!

Top comments (0)