Create a Dynamic Modal using PHP and HTMX #1
Tags: devjournal, htmx, php, webdev
Published: 2024-03-27 17:31:00

Hello Punk! This is the index #1

{% embed https://dev.to/darkterminal/create-a-dynamic-modal-using-php-and-htmx-44j5 %}

Well, I am still at the point where I left before. Yes! The next part about:

## The Head Voice! 

> How can I create a modal that display form to create a new supplier in my app and load the content from backend (Yes! PHP) that generate the content, also doing validation and stuff, and removing the modal after the create operation is done!.

![Jd Headless Jd GIF - https://tenor.com/view/jd-headless-jd-john-dorian-bye-funny-gif-14134807](https://media1.tenor.com/m/ifAqVZ3TOlcAAAAd/jd-headless-jd.gif)

---

After I create a the modal and displaying form with awesome CSS transition (copy & paste) from htmx docs. Then what I need is make that form work with insert action.

## The Routing #2 POST

To make the form work with backend I need to create 1 more route that handle POST request from client to server.

```php
<?php
// Filename: /home/darkterminal/workspaces/fck-htmx/routes/web.php
use Fckin\core\Application;

/** @var Application $app  */

$app->router->get('suppliers/create', 'Suppliers@create');
$app->router->post('suppliers/create', 'Suppliers@create');
```

_Myself: Why you don't use match routing method? Like:_

```php
$app-router->match(['GET','POST'], 'suppliers/create', 'Suppliers@create')
```

Aaaah... I won't! If you want, create for yourself! (btw, it's not available in my `fck` PHP Framework, cz I am too egoist). Don't ask about the my `fck` PHP Framework documentation, they doesn't exists.

## The Controller #2 POST

Still in the same file, but need lil tweak! ✨

The controller before

```php
<?php
// Filename: /home/darkterminal/workspaces/fck-htmx/controllers/Suppliers.php

namespace App\controllers;

use App\config\helpers\Utils;
use App\models\Suppliers as ModelsSuppliers;
use Fckin\core\Controller;
use Fckin\core\Request;
use Fckin\core\Response;

class Suppliers extends Controller
{
    protected $suppliers;

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

public function create(Request $request)
    {
        $params = []; // <-- I will do it something here when make POST request

        return Utils::addComponent('suppliers/modals/modal-create', $params);
    }
}
```

The controller after

```php
<?php
// Filename: /home/darkterminal/workspaces/fck-htmx/controllers/Suppliers.php

namespace App\controllers;

use App\config\helpers\Utils;
use App\models\Suppliers as ModelsSuppliers;
use Fckin\core\Controller;
use Fckin\core\Request;
use Fckin\core\Response;

class Suppliers extends Controller
{
    protected $suppliers;

    public function __construct()
    {
        $response = new Response();
        if (!isAuthenticate()) {
            $response->setStatusCode(401);
            exit();
        }
        $this->suppliers = new ModelsSuppliers();
    }
    
    public function create(Request $request)
    {
        $params = [
            'errors' => []
        ];
        if ($request->isPost()) {
            $formData = $request->getBody();
            $formData['supplierCoordinate'] = empty($formData['latitude']) || empty($formData['latitude']) ? null : \implode(',', [$formData['latitude'], $formData['longitude']]);
            $formData = Utils::remove_keys($formData, ['latitude', 'longitude']);
            $this->suppliers->loadData($formData);
            if ($this->suppliers->validate() && $this->suppliers->create($formData)) {
                \header('HX-Trigger: closeModal, loadTableSupplier');
                 exit();
            } else {
                $params['errors'] = $this->suppliers->errors;
            }
        }

        return Utils::addComponent('suppliers/modals/modal-create', $params);
    }
}
```

Emmm.... deez neat! How the `App\models\Suppliers as ModelsSuppliers` look like? Did you mean the Model? Sure whatever you want! But before going further... I need to breakdown deez controller first.

```php
$this->suppliers = new ModelsSuppliers();
```

Initialized the supplier model that accessible in the entire Class Controller from the `__constructor` method. Then I modify the `$params` variable

```php
$params = [
    'errors' => []
];
```

I add the `errors` key that store the error messages from validation. The validation part I will explain in the next paragraph, so the story will be inline as possible. Trust me!

```php
if ($request->isPost())
```

Check if the client request to `POST` method, if yes then collect the payload from that request using

```php
$formData = $request->getBody();
```

and store into `$formData` variable. Because I am handsome and stupid I create field for `latitude` and `longitude` separately and don't comment about the ternary `if else` statement! Pleaaaaseee....

```php
$formData['supplierCoordinate'] = empty($formData['latitude']) || empty($formData['latitude']) ? null : \implode(',', [$formData['latitude'], $formData['longitude']]);
``` 

So I can combine the `latitude` and `longitude` as a `supplierCoordinate` value. Then remove the `latitude` and `longitude` from `payload` cz I don't need anymore.

```php
$formData = Utils::remove_keys($formData, ['latitude', 'longitude']);
```

Also I load that `payload` into the model

```php
$this->suppliers->loadData($formData);
```

So the model can read all the payload and validate the `payload`

```php
$this->suppliers->validate()
```

In the background I have the rules for the input. They look like this:

```php
public function rules(): array
{
    return [
        'supplierName' => [self::RULE_REQUIRED],
        'supplierCompanyName' => [self::RULE_REQUIRED],
        'supplierAddress' => [self::RULE_REQUIRED],
        'supplierPhoneNumber' => [self::RULE_REQUIRED],
        'supplierEmail' => [self::RULE_EMAIL],
        'supplierCoordinate' => []
    ];
}
```

If validation passed! Then create the new supplier from the `payload`

```php
$this->suppliers->create($formData)
```

If both of them is passed then send the trigger event to the client from the backend

```php
\header('HX-Trigger: closeModal, loadTableSupplier');
```

_Wait! Wait!! Wait!!! Please... tell me where what the `loadTableSupplier`?! Where is the table!_

Sorry for that... I will explain. Please be patient...

The `HX-Trigger` is the way how htmx communicate between server and client. This trigger place into the response headers. then telling the client to react on that event.

and if one of them (the validate and create) method doesn't passed then

```php
$params['errors'] = $this->suppliers->errors;
```

get the error messages to the response payload.

```php
return Utils::addComponent('suppliers/modals/modal-create', $params);
```

the `Utils::addComponent` is a glue and part of **hypermedia content** that can deliver to the client instead sending [fixed-format JSON Data APIs](https://www.infoworld.com/article/3713164/complexity-bad-an-interview-with-carson-gross.html).

## Error Validation Message

Did you remember the `Utils::getValidationError` method in my form?

```php
Utils::getValidationError($errors, 'supplierName')
```

In each form field? Yes, that the draw how server and client exchange the dynamic hypermedia content. Oh My Punk!

![Aaaah Too handsome](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/auhwjb4u00o6r0172f6n.png)

Whenever the `$errors` is empty the message isn't appear in that form, but if the `$errors` variable is not empty then it will display the validation message.

![The Validation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2wjg3uv781g03m4qhjo6.gif)

## Where Is The Table?

![Angry](https://media1.tenor.com/m/ITYG8B1QySIAAAAC/roman-reigns-no.gif)

Hold on... don't look at me like that! _this is wasting my time_. I know, when you choose this topic, you willing to read this and wasting your time also.

Here is the table, but don't complain about the Tailwind Classes and everything is wide and long to read to the right side 🤣 Oh My Punk! this is funny... sorry, I regret. Cz this  table is look identical with

{% embed https://dev.to/darkterminal/1-endpoint-do-5-things-for-htmx-datatable-1k57 %}

```html
<?php

use App\config\helpers\Icons;
use App\config\helpers\Utils;

$queries = [];
parse_str($suppliers['queryString'], $queries);

$currentPage = array_replace_recursive($queries, ['page' => 1, 'limit' => $suppliers['totalRows']]);
$prevPage = array_replace_recursive($queries, ['page' => $suppliers['currentPage'] - 1, 'search' => '']);
$nextPage = array_replace_recursive($queries, ['page' => $suppliers['currentPage'] + 1, 'search' => '']);
?>
<div class="overflow-x-auto" id="suppliers-table" hx-trigger="loadTableSupplier from:body" hx-get="<?= base_url('suppliers?' . http_build_query(array_merge($currentPage, ['column' => 'suppliers.updatedAt', 'limit' => 10, 'search' => '', 'order' => 'desc']))) ?>" hx-target="this" hx-swap="outerHTML">
    <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">Suppliers</h2>
        <div class="flex gap-3">
            <input type="search" name="search" placeholder="Search here..." id="search" value="<?= $suppliers['search'] ?? '' ?>" class="w-80 input input-sm input-bordered focus:outline-none" hx-get="<?= base_url('suppliers?' . http_build_query(array_merge($currentPage, ['column' => 'suppliers.supplierId']))) ?>" hx-trigger="input changed delay:500ms, search" hx-swap="outerHTML" hx-target="#suppliers-table" hx-indicator="#table-indicator" autocomplete="off" />
            <button class="btn btn-sm btn-neutral" hx-get="<?= base_url('suppliers/create') ?>" hx-target="body" hx-swap="beforeend" id="exclude-indicator"><?= Icons::use('Plus', 'h-4 w-4') ?>Create new</button>
        </div>
    </div>
    <table class="table table-zebra table-sm">
        <thead>
            <tr>
                <th <?= Utils::buildHxAttributes('suppliers', $suppliers['queryString'], 'suppliers.supplierId', $suppliers['activeColumn'], 'suppliers-table') ?>>#</th>
                <th <?= Utils::buildHxAttributes('suppliers', $suppliers['queryString'], 'suppliers.supplierName', $suppliers['activeColumn'], 'suppliers-table') ?>>Supplier Name</th>
                <th <?= Utils::buildHxAttributes('suppliers', $suppliers['queryString'], 'suppliers.supplierCompanyName', $suppliers['activeColumn'], 'suppliers-table') ?>>Company Name</th>
                <th <?= Utils::buildHxAttributes('suppliers', $suppliers['queryString'], 'suppliers.supplierPhoneNumber', $suppliers['activeColumn'], 'suppliers-table') ?>>Phone Number</th>
                <th <?= Utils::buildHxAttributes('suppliers', $suppliers['queryString'], 'suppliers.supplierAddress', $suppliers['activeColumn'], 'suppliers-table') ?>>Address</th>
                <th class="cursor-not-allowed">Status</th>
                <th class="cursor-not-allowed">Distance</th>
                <th class="cursor-not-allowed">#</th>
            </tr>
        </thead>
        <tbody>
            <?php foreach ($suppliers['data'] as $supplier) : ?>
                <tr>
                    <th>S#<?= $supplier->supplierId ?></th>
                    <td><?= $supplier->supplierName ?></td>
                    <td><?= $supplier->supplierCompanyName ?></td>
                    <td><?= $supplier->supplierPhoneNumber ?></td>
                    <td><?= $supplier->supplierAddress ?></td>
                    <td><?= $supplier->isActive ? 'Active' : 'Inactive' ?></td>
                    <td>
                        <?php
                        if (!empty($supplier->supplierCoordinate)) {
                            $supplierSource = explode(',', $supplier->supplierCoordinate);
                            $appSource = explode(',', '-6.444508061297425,111.01966363196293');
                            echo round(Utils::haversine(
                                [
                                    'lat' => $supplierSource[0],
                                    'long' => $supplierSource[1],
                                ],
                                [
                                    'lat' => $appSource[0],
                                    'long' => $appSource[1],
                                ]
                            )) . " Km";
                        } else {
                            echo "Not set";
                        }
                        ?>
                    </td>
                    <td>
                        <div class="flex gap-2">
                            <button class="text-white bg-blue-600 btn btn-xs hover:bg-blue-700 tooltip tooltip-top" data-tip="View Detail" hx-get="<?= base_url('suppliers/detail/' . $supplier->supplierId) ?>" hx-target="body" hx-swap="beforeend" id="exclude-indicator"><?= Icons::use('Eye', 'h-4 w-4') ?></button>
                            <button class="text-white bg-green-600 btn btn-xs hover:bg-green-700 tooltip tooltip-top" data-tip="Edit Detail" hx-get="<?= base_url('suppliers/edit/' . $supplier->supplierId) ?>" hx-target="body" hx-swap="beforeend" id="exclude-indicator"><?= Icons::use('Pencil', 'h-4 w-4') ?></button>
                            <button class="text-white btn btn-xs <?= $supplier->isActive ? 'bg-gray-600 hover:bg-gray-700' : 'bg-blue-600 hover:bg-blue-700' ?> tooltip tooltip-top" data-tip="<?= $supplier->isActive ? 'Deactived' : 'Activated' ?>" hx-post="<?= base_url($supplier->isActive ? 'suppliers/deactivated/' : 'suppliers/activated/') . $supplier->supplierId . '?' . http_build_query(array_merge($currentPage, ['column' => 'suppliers.supplierId', 'limit' => 10, 'search' => ''])) ?>" hx-target="#suppliers-table" hx-swap="outerHTML" hx-indicator="#table-indicator" hx-confirm="Are you sure?"><?= Icons::use($supplier->isActive ? 'XCircle' : 'CheckCircle', 'h-4 w-4') ?></button>
                            <button class="text-white btn btn-xs <?= $supplier->isActive ? 'bg-red-600 hover:bg-red-700' : 'bg-blue-600 hover:bg-blue-700' ?> tooltip tooltip-top" data-tip="Delete" hx-delete="<?= base_url('suppliers/delete/') . $supplier->supplierId . '?' . http_build_query(array_merge($currentPage, ['column' => 'suppliers.supplierId', 'limit' => 10, 'search' => ''])) ?>" hx-target="#suppliers-table" hx-swap="outerHTML" hx-indicator="#table-indicator" hx-confirm="Are you sure want to delete <?= $supplier->supplierName ?>?"><?= Icons::use('Trash', 'h-4 w-4') ?></button>
                        </div>
                    </td>
                </tr>
            <?php endforeach; ?>
        </tbody>
    </table>
    <div class="flex flex-row justify-between mt-3">
        <p>
            Page <span class="font-bold"><?= $suppliers['currentPage'] ?></span> from <span class="font-bold"><?= $suppliers['totalPages'] ?></span> Total <span class="font-bold"><?= $suppliers['totalRows'] ?></span> |
            Jump to: <input type="number" name="pageNumber" id="pageNumber" hx-on:change="var url = '<?= base_url('suppliers?' . http_build_query(array_merge($prevPage, ['column' => 'suppliers.supplierId', 'search' => '']))) ?>';
                var replacedUrl = url.replace(/page=\d+/, 'page=' + this.value);
                htmx.ajax('GET', replacedUrl, {target: '#suppliers-table', swap: 'outerHTML'})" class="w-12 input input-sm input-bordered" min="1" max="<?= $suppliers['totalPages'] ?>" value="<?= $suppliers['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('suppliers?' . http_build_query(array_merge($prevPage, ['column' => 'suppliers.supplierId']))) ?>'
                    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: '#suppliers-table', swap: 'outerHTML'})
                ">
                <option <?= $suppliers['limit'] == 10 ? 'selected' : '' ?> value="10">10 Rows</option>
                <option <?= $suppliers['limit'] == 20 ? 'selected' : '' ?> value="20">20 Rows</option>
                <option <?= $suppliers['limit'] == 30 ? 'selected' : '' ?> value="30">30 Rows</option>
                <option <?= $suppliers['limit'] == 40 ? 'selected' : '' ?> value="40">40 Rows</option>
                <option <?= $suppliers['limit'] == 50 ? 'selected' : '' ?> value="50">50 Rows</option>
            </select>
        </p>
        <div class="join">
            <button class="join-item btn btn-sm" <?= Utils::hxPagination('suppliers', http_build_query(array_merge($prevPage, ['column' => 'suppliers.supplierId'])), 'suppliers-table') ?> <?= ($suppliers['currentPage'] <= 1) ? 'disabled' : '' ?>>«</button>
            <button class="join-item btn btn-sm">Page <?= $suppliers['currentPage'] ?></button>
            <button class="join-item btn btn-sm" <?= Utils::hxPagination('suppliers', http_build_query(array_merge($nextPage, ['column' => 'suppliers.supplierId'])), 'suppliers-table') ?> <?= $suppliers['currentPage'] >= $suppliers['totalPages'] ? 'disabled' : '' ?>>»</button>
        </div>
    </div>
</div>
```
In that table what I want to highlight is just the first div!

```html
<div 
	class="overflow-x-auto"
    id="suppliers-table"
    hx-trigger="loadTableSupplier from:body"
    hx-get="<?= base_url('suppliers?' . http_build_query(array_merge($currentPage, ['column' => 'suppliers.updatedAt', 'limit' => 10, 'search' => '', 'order' => 'desc']))) ?>"
    hx-target="this"
    hx-swap="outerHTML"
>
```

![laugh](https://media1.tenor.com/m/Nf4lH38DqMkAAAAd/dgn-vlog.gif)

🤣 Yes! The `hx` attributes:

- `hx-trigger="loadTableSupplier from:body"` is responsible to receive the `loadTableSupplier` event that sent from the server and come to the client page `from:body`
- then the `hx-get` with their long value that really mess up! But the `hx-get` will get the latest content from server
- and the `hx-target` will placed the content to the element
- and the `hx-swap` will replace the entire div with the latest content also display the new supplier inside

🤯 Boom!!!

## The Model

The model look pretty clean... and I hate it so much cz I forgot how it's work!

> Simple things sometime make me blind. But in the chaotic things, I see the pattern. _.darkterminal_

```php
<?php

namespace App\models;

use Fckin\core\db\Model;

class Suppliers extends Model
{
    public string $supplierName;
    public string $supplierCompanyName;
    public string $supplierAddress;
    public string $supplierPhoneNumber;
    public string|null $supplierEmail;
    public string|null $supplierCoordinate;

    public string $tableName = 'suppliers';

    public function rules(): array
    {
        return [
            'supplierName' => [self::RULE_REQUIRED],
            'supplierCompanyName' => [self::RULE_REQUIRED],
            'supplierAddress' => [self::RULE_REQUIRED],
            'supplierPhoneNumber' => [self::RULE_REQUIRED],
            'supplierEmail' => [self::RULE_EMAIL],
            'supplierCoordinate' => []
        ];
    }

    public function create(array $data): bool
    {
        $created = $this->table($this->tableName)->insert($data);
        return $created > 0 ? true : false;
    }
}
```

You can look at `thc-fck` core of my PHP Framework about the [`Fckin\core\db\Model`](https://github.com/darkterminal/the-fck/blob/main/core/db/Model.php). That's it!

😊 Sorry for wasting your time...