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...
1<!-- Filename: fck-htmx/views/components/dashboard/partials/customers.approval/table.php -->
2<div class="overflow-x-auto" id="customers-table">
3 <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>
4 <div class="flex flex-row justify-between mb-3">
5 <h2 class="card-title">Customer Approval</h2>
6 <input
7 type="search"
8 name="search"
9 placeholder="Search here..."
10 id="search"
11 value="<?= $customers['search'] ?>"
12 class="w-80 input input-sm input-bordered focus:outline-none"
13 hx-get="<?= base_url('customers?' . http_build_query(array_merge($currentPage, ['column' => 'customers.customerId']))) ?>"
14 hx-trigger="input changed delay:500ms, search"
15 hx-swap="outerHTML"
16 hx-target="#customers-table"
17 hx-indicator="#table-indicator"
18 autocomplete="off"
19 />
20 </div>
21 <table class="table table-zebra">
22 <thead>
23 <tr>
24 <th <?= buildHxAttributes('customers', $customers['queryString'], 'customers.customerId', $customers['activeColumn'], 'customers-table') ?>>#</th>
25 <th <?= buildHxAttributes('customers', $customers['queryString'], 'customers.customerName', $customers['activeColumn'], 'customers-table') ?>>Customer Name</th>
26 <th <?= buildHxAttributes('customers', $customers['queryString'], 'profiles.firstName', $customers['activeColumn'], 'customers-table') ?>>Marketing Executive</th>
27 <th <?= buildHxAttributes('customers', $customers['queryString'], 'customers.customerStoreName', $customers['activeColumn'], 'customers-table') ?>>Store Name</th>
28 <th <?= buildHxAttributes('customers', $customers['queryString'], 'customers.customerAddress', $customers['activeColumn'], 'customers-table') ?>>Address</th>
29 <th class="cursor-not-allowed">Status</th>
30 <th class="cursor-not-allowed">Distance</th>
31 </tr>
32 </thead>
33 <tbody>
34 <?php foreach ($customers['data'] as $customer) : ?>
35 <tr>
36 <th>C#<?= $customer->customerId ?></th>
37 <td><?= $customer->customerName ?></td>
38 <td><?= $customer->firstName . ' ' . $customer->lastName ?></td>
39 <td><?= $customer->customerStoreName ?></td>
40 <td><?= $customer->customerAddress ?></td>
41 <td><?= $customer->isActived ? 'Active' : 'Inactive' ?></td>
42 <td>
43 <?php
44 $customerSource = explode(',', $customer->customerCoordinate);
45 $appSource = explode(',', '-7.7682121,110.4083341');
46 echo round(Utils::haversine(
47 [
48 'lat' => $customerSource[0],
49 'long' => $customerSource[1],
50 ],
51 [
52 'lat' => $appSource[0],
53 'long' => $appSource[1],
54 ]
55 )) . " Km";
56 ?>
57 </td>
58 </tr>
59 <?php endforeach; ?>
60 </tbody>
61 </table>
62 <div class="flex flex-row justify-between mt-3">
63 <p>
64 Page <span class="font-bold"><?= $customers['currentPage'] ?></span> from <span class="font-bold"><?= $customers['totalPages'] ?></span> Total <span class="font-bold"><?= $customers['totalRows'] ?></span> |
65 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' => '']))) ?>';
66 var replacedUrl = url.replace(/page=\d+/, 'page=' + this.value);
67 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" />
68 Display: <select
69 class="w-48 select select-bordered select-sm"
70 hx-indicator="#table-indicator"
71 hx-on:change="var url = '<?= base_url('customers?' . http_build_query(array_merge($prevPage, ['column' => 'customers.customerId']))) ?>'
72 var pageNumber = parseInt('<?= $prevPage['page'] ?>') == 0 ? 1 : parseInt('<?= $prevPage['page'] ?>')
73 var replacedUrl = url.replace(/limit=\d+/, 'limit=' + this.value);
74 htmx.ajax('GET', replacedUrl.replace(/page=\d+/, 'page=' + pageNumber), {target: '#customers-table', swap: 'outerHTML'})
75 ">
76 <option <?= $customers['limit'] == 10 ? 'selected' : '' ?> value="10">10 Rows</option>
77 <option <?= $customers['limit'] == 20 ? 'selected' : '' ?> value="20">20 Rows</option>
78 <option <?= $customers['limit'] == 30 ? 'selected' : '' ?> value="30">30 Rows</option>
79 <option <?= $customers['limit'] == 40 ? 'selected' : '' ?> value="40">40 Rows</option>
80 <option <?= $customers['limit'] == 50 ? 'selected' : '' ?> value="50">50 Rows</option>
81 </select>
82 </p>
83 <div class="join">
84 <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>
85 <button class="join-item btn btn-sm">Page <?= $customers['currentPage'] ?></button>
86 <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>
87 </div>
88 </div>
89</div>
๐ฒ 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 #
1<input
2 type="search"
3 name="search"
4 placeholder="Search here..."
5 id="search"
6 value="<?= $customers['search'] ?>"
7 class="w-80 input input-sm input-bordered focus:outline-none"
8 hx-get="<?= base_url('customers?' . http_build_query(array_merge($currentPage, ['column' => 'customers.customerId']))) ?>"
9 hx-trigger="input changed delay:500ms, search"
10 hx-swap="outerHTML"
11 hx-target="#customers-table"
12 hx-indicator="#table-indicator"
13 autocomplete="off"
14/>
What you see? Yes, the f hx-
attribute!
hx-get
to issues aGET
to the specified URLhx-swap
to controls how content will swap in (outerHTML
,beforeend
,afterend
, โฆ)hx-target
to specifies the target element to be swappedhx-indicator
the element to put thehtmx-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 #
1<tr>
2 <th <?= buildHxAttributes('customers', $customers['queryString'], 'customers.customerId', $customers['activeColumn'], 'customers-table') ?>>#</th>
3 <th <?= buildHxAttributes('customers', $customers['queryString'], 'customers.customerName', $customers['activeColumn'], 'customers-table') ?>>Customer Name</th>
4 <th <?= buildHxAttributes('customers', $customers['queryString'], 'profiles.firstName', $customers['activeColumn'], 'customers-table') ?>>Marketing Executive</th>
5 <th <?= buildHxAttributes('customers', $customers['queryString'], 'customers.customerStoreName', $customers['activeColumn'], 'customers-table') ?>>Store Name</th>
6 <th <?= buildHxAttributes('customers', $customers['queryString'], 'customers.customerAddress', $customers['activeColumn'], 'customers-table') ?>>Address</th>
7 <th class="cursor-not-allowed">Status</th>
8 <th class="cursor-not-allowed">Distance</th>
9</tr>
๐ 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...
1function buildHxAttributes($base_url, $queryString, $column, $activeColumn, $targetId, $search = '', $extraClasses = '')
2{
3 $hxAttributes = 'hx-get="' . base_url($base_url . '?' . $queryString . '&column=' . $column . '&search=' . $search) . '" ';
4 $hxAttributes .= 'hx-swap="outerHTML" ';
5 $hxAttributes .= 'hx-target="#' . $targetId . '" ';
6 $hxAttributes .= 'hx-indicator="#table-indicator" ';
7 $hxAttributes .= 'class="cursor-pointer ';
8 if ($activeColumn === $column) {
9 $hxAttributes .= 'bg-base-300 ';
10 }
11 $hxAttributes .= $extraClasses . '"';
12
13 return $hxAttributes;
14}
๐ฒ Ooooooooooohhh...
Can I...
๐ฒ Yes please...
Markup #3: The Trampoline #
1Jump to:
2 <input
3 type="number"
4 name="pageNumber"
5 id="pageNumber"
6 hx-on:change="var url = '<?= base_url('customers?' . http_build_query(array_merge($prevPage, ['column' => 'customers.customerId', 'search' => '']))) ?>';
7 var replacedUrl = url.replace(/page=\d+/, 'page=' + this.value);
8 htmx.ajax('GET', replacedUrl, {target: '#customers-table', swap: 'outerHTML'})"
9 class="w-12 input input-sm input-bordered"
10 min="1"
11 max="<?= $customers['totalPages'] ?>"
12 value="<?= $customers['currentPage'] ?>"
13 hx-indicator="#table-indicator"
14 />
Did you just write PHP and JavaScript inside the attribute like this, inline?
1hx-on:change="
2 var url = '<?= base_url('customers?' . http_build_query(array_merge($prevPage, ['column' => 'customers.customerId', 'search' => '']))) ?>';
3 var replacedUrl = url.replace(/page=\d+/, 'page=' + this.value);
4 htmx.ajax('GET', replacedUrl, {target: '#customers-table', swap: 'outerHTML'})
5"
Yes... that's call "use client & server";
, sorry I am joking! Please give me_ ๐ ๐ ๐
Markup #4: Display Limit #
This is the select options field to allow user choose limit data that displayed in the table. Hmmmm... sound promising! ๐คฃ
1Display: <select
2 class="w-48 select select-bordered select-sm"
3 hx-indicator="#table-indicator"
4 hx-on:change="var url = '<?= base_url('customers?' . http_build_query(array_merge($prevPage, ['column' => 'customers.customerId']))) ?>'
5 var pageNumber = parseInt('<?= $prevPage['page'] ?>') == 0 ? 1 : parseInt('<?= $prevPage['page'] ?>')
6 var replacedUrl = url.replace(/limit=\d+/, 'limit=' + this.value);
7 htmx.ajax('GET', replacedUrl.replace(/page=\d+/, 'page=' + pageNumber), {target: '#customers-table', swap: 'outerHTML'})
8 ">
9 <option <?= $customers['limit'] == 10 ? 'selected' : '' ?> value="10">10 Rows</option>
10 <option <?= $customers['limit'] == 20 ? 'selected' : '' ?> value="20">20 Rows</option>
11 <option <?= $customers['limit'] == 30 ? 'selected' : '' ?> value="30">30 Rows</option>
12 <option <?= $customers['limit'] == 40 ? 'selected' : '' ?> value="40">40 Rows</option>
13 <option <?= $customers['limit'] == 50 ? 'selected' : '' ?> value="50">50 Rows</option>
14</select>
Aaaaaaaaaaand again! JavaScript code inside the attribute value ๐ this is funny right!?
Markup #5: The Pagination #
1<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>
2<button class="join-item btn btn-sm">Page <?= $customers['currentPage'] ?></button>
3<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>
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!
1function hxPagination($base_url, $queryString, $targetId)
2{
3 $hxAttributes = 'hx-get="' . base_url($base_url . '?' . $queryString) . '" ';
4 $hxAttributes .= 'hx-swap="outerHTML" ';
5 $hxAttributes .= 'hx-target="#' . $targetId . '" ';
6 $hxAttributes .= 'hx-indicator="#table-indicator" ';
7
8 return $hxAttributes;
9}
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 #
1<?php
2// Filename: fck-htmx/routes/web.php
3
4use Fckin\core\Application;
5
6/** @var Application $app */
7
8$app->router->get('/', 'Customers@datatable');
Router done!
The Controller #
1<?php
2// Filename: fck-htmx/controllers/Customers.php
3
4namespace App\controllers;
5
6use App\config\helpers\Utils;
7use App\models\Customers as ModelsCustomers;
8use Fckin\core\Controller;
9use Fckin\core\Request;
10use Fckin\core\Response;
11
12class Customers extends Controller
13{
14 protected $customers;
15
16 public function __construct()
17 {
18 $response = new Response();
19 if (!isAuthenticate()) {
20 $response->setStatusCode(401);
21 exit();
22 }
23 $this->customers = new ModelsCustomers();
24 }
25
26 public function datatable(Request $request)
27 {
28 $customers = $this->customers->notActiveDataTables(
29 $request->getQuery('column'),
30 \strtoupper($request->getQuery('order')),
31 $request->getQuery('page'),
32 $request->getQuery('limit'),
33 $request->getQuery('search')
34 );
35 $direction = $request->getQuery('order') === 'asc' ? 0 : 1;
36 $params = [
37 'customers' => [
38 ...$customers,
39 'search' => $request->getQuery('search'),
40 'activeColumn' => $request->getQuery('column'),
41 'order' => $direction,
42 'page' => (int) $request->getQuery('page'),
43 'limit' => (int) $request->getQuery('limit'),
44 'queryString' => \http_build_query([
45 'order' => $direction === 0 ? 'desc' : 'asc',
46 'page' => $request->getQuery('page'),
47 'limit' => $request->getQuery('limit')
48 ])
49 ]
50 ];
51 return Utils::addComponent('dashboard/partials/customers.approval/table', $params);
52 }
53}
Controller is done!
The Model #
1<?php
2// Filename: fck-htmx/models/Customers.php
3
4namespace App\models;
5
6use Fckin\core\db\Model;
7use PDO;
8
9class Customers extends Model
10{
11 public function notActiveDataTables(string $column = 'customers.customerId', string $order_by = 'ASC', int $page = 1, int $limit = 10, string $search = ''): array
12 {
13 $totalRows = $this->executeQuery('customers_all_is_inactive')->rowCount();
14
15 $limit = empty($search) && ($limit === 10 || $limit === $totalRows) ? 10 : $limit;
16
17 $offset = ($page - 1) * $limit;
18
19 $sql = "SELECT
20 customers.*,
21 users.*,
22 roles.*,
23 profiles.*
24 FROM
25 customers
26 JOIN users ON customers.userId = users.userId
27 JOIN roles ON users.roleId = roles.roleId
28 JOIN profiles ON profiles
29 .userId = users.userId
30 WHERE
31 customers.isActived = 0 AND (
32 customers.customerName LIKE '%{$search}%' OR profiles.firstName LIKE '%{$search}%' OR profiles.lastName LIKE '%{$search}%'
33 )
34 ORDER BY
35 {$column} {$order_by}
36 LIMIT {$offset}, {$limit}";
37 $results = $this->query($sql);
38
39 $totalPages = ceil($totalRows / $limit);
40
41 return [
42 'currentPage' => $page,
43 'nextPage' => min($page + 1, $totalPages),
44 'totalPages' => $totalPages,
45 'offset' => $offset,
46 'totalRows' => $totalRows,
47 'data' => $results->fetchAll(PDO::FETCH_OBJ)
48 ];
49 }
50}
Model is done!
That's it! Here the result: https://youtu.be/MEhqJpLoZJI