Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Imam Ali Mustofa
Imam Ali Mustofa

Posted on • Edited on • Originally published atdarkterminal.prose.sh

     

Create a Dynamic Modal using PHP and HTMX #1

Hello Punk! This is the index #1

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


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// Filename: /home/darkterminal/workspaces/fck-htmx/routes/web.phpuseFckin\core\Application;/** @var Application $app  */$app->router->get('suppliers/create','Suppliers@create');$app->router->post('suppliers/create','Suppliers@create');
Enter fullscreen modeExit fullscreen mode

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

$app-router->match(['GET','POST'],'suppliers/create','Suppliers@create')
Enter fullscreen modeExit fullscreen mode

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

The Controller #2 POST

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

The controller before

<?php// Filename: /home/darkterminal/workspaces/fck-htmx/controllers/Suppliers.phpnamespaceApp\controllers;useApp\config\helpers\Utils;useApp\models\SuppliersasModelsSuppliers;useFckin\core\Controller;useFckin\core\Request;useFckin\core\Response;classSuppliersextendsController{protected$suppliers;publicfunction__construct(){$response=newResponse();if(!isAuthenticate()){$response->setStatusCode(401);exit();}$this->suppliers=newModelsSuppliers();}publicfunctioncreate(Request$request){$params=[];// <-- I will do it something here when make POST requestreturnUtils::addComponent('suppliers/modals/modal-create',$params);}}
Enter fullscreen modeExit fullscreen mode

The controller after

<?php// Filename: /home/darkterminal/workspaces/fck-htmx/controllers/Suppliers.phpnamespaceApp\controllers;useApp\config\helpers\Utils;useApp\models\SuppliersasModelsSuppliers;useFckin\core\Controller;useFckin\core\Request;useFckin\core\Response;classSuppliersextendsController{protected$suppliers;publicfunction__construct(){$response=newResponse();if(!isAuthenticate()){$response->setStatusCode(401);exit();}$this->suppliers=newModelsSuppliers();}publicfunctioncreate(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;}}returnUtils::addComponent('suppliers/modals/modal-create',$params);}}
Enter fullscreen modeExit fullscreen mode

Emmm.... deez neat! How theApp\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.

$this->suppliers=newModelsSuppliers();
Enter fullscreen modeExit fullscreen mode

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

$params=['errors'=>[]];
Enter fullscreen modeExit fullscreen mode

I add theerrors 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!

if($request->isPost())
Enter fullscreen modeExit fullscreen mode

Check if the client request toPOST method, if yes then collect the payload from that request using

$formData=$request->getBody();
Enter fullscreen modeExit fullscreen mode

and store into$formData variable. Because I am handsome and stupid I create field forlatitude andlongitude separately and don't comment about the ternaryif else statement! Pleaaaaseee....

$formData['supplierCoordinate']=empty($formData['latitude'])||empty($formData['latitude'])?null:\implode(',',[$formData['latitude'],$formData['longitude']]);
Enter fullscreen modeExit fullscreen mode

So I can combine thelatitude andlongitude as asupplierCoordinate value. Then remove thelatitude andlongitude frompayload cz I don't need anymore.

$formData=Utils::remove_keys($formData,['latitude','longitude']);
Enter fullscreen modeExit fullscreen mode

Also I load thatpayload into the model

$this->suppliers->loadData($formData);
Enter fullscreen modeExit fullscreen mode

So the model can read all the payload and validate thepayload

$this->suppliers->validate()
Enter fullscreen modeExit fullscreen mode

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

publicfunctionrules():array{return['supplierName'=>[self::RULE_REQUIRED],'supplierCompanyName'=>[self::RULE_REQUIRED],'supplierAddress'=>[self::RULE_REQUIRED],'supplierPhoneNumber'=>[self::RULE_REQUIRED],'supplierEmail'=>[self::RULE_EMAIL],'supplierCoordinate'=>[]];}
Enter fullscreen modeExit fullscreen mode

If validation passed! Then create the new supplier from thepayload

$this->suppliers->create($formData)
Enter fullscreen modeExit fullscreen mode

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

\header('HX-Trigger: closeModal, loadTableSupplier');
Enter fullscreen modeExit fullscreen mode

Wait! Wait!! Wait!!! Please... tell me where what theloadTableSupplier?! Where is the table!

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

TheHX-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

$params['errors']=$this->suppliers->errors;
Enter fullscreen modeExit fullscreen mode

get the error messages to the response payload.

returnUtils::addComponent('suppliers/modals/modal-create',$params);
Enter fullscreen modeExit fullscreen mode

theUtils::addComponent is a glue and part ofhypermedia content that can deliver to the client instead sendingfixed-format JSON Data APIs.

Error Validation Message

Did you remember theUtils::getValidationError method in my form?

Utils::getValidationError($errors,'supplierName')
Enter fullscreen modeExit fullscreen mode

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

Aaaah Too handsome

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

Where Is The Table?

Angry

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

<?phpuse 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' => '']);?><divclass="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"><divclass="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><divclass="flex flex-row justify-between mb-3"><h2class="card-title">Suppliers</h2><divclass="flex gap-3"><inputtype="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"/><buttonclass="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><tableclass="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><thclass="cursor-not-allowed">Status</th><thclass="cursor-not-allowed">Distance</th><thclass="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><divclass="flex gap-2"><buttonclass="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><buttonclass="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><buttonclass="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><buttonclass="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><divclass="flex flex-row justify-between mt-3"><p>            Page<spanclass="font-bold"><?= $suppliers['currentPage'] ?></span> from<spanclass="font-bold"><?= $suppliers['totalPages'] ?></span> Total<spanclass="font-bold"><?= $suppliers['totalRows'] ?></span> |            Jump to:<inputtype="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:<selectclass="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><divclass="join"><buttonclass="join-item btn btn-sm"<?=Utils::hxPagination('suppliers',http_build_query(array_merge($prevPage,['column'=> 'suppliers.supplierId'])), 'suppliers-table') ?><?= ($suppliers['currentPage'] <= 1) ? 'disabled' : '' ?></button><buttonclass="join-item btn btn-sm">Page<?= $suppliers['currentPage'] ?></button><buttonclass="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>
Enter fullscreen modeExit fullscreen mode

In that table what I want to highlight is just the first div!

<divclass="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">
Enter fullscreen modeExit fullscreen mode

laugh

🤣 Yes! Thehx attributes:

  • hx-trigger="loadTableSupplier from:body" is responsible to receive theloadTableSupplier event that sent from the server and come to the client pagefrom:body
  • then thehx-get with their long value that really mess up! But thehx-get will get the latest content from server
  • and thehx-target will placed the content to the element
  • and thehx-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

<?phpnamespaceApp\models;useFckin\core\db\Model;classSuppliersextendsModel{publicstring$supplierName;publicstring$supplierCompanyName;publicstring$supplierAddress;publicstring$supplierPhoneNumber;publicstring|null$supplierEmail;publicstring|null$supplierCoordinate;publicstring$tableName='suppliers';publicfunctionrules():array{return['supplierName'=>[self::RULE_REQUIRED],'supplierCompanyName'=>[self::RULE_REQUIRED],'supplierAddress'=>[self::RULE_REQUIRED],'supplierPhoneNumber'=>[self::RULE_REQUIRED],'supplierEmail'=>[self::RULE_EMAIL],'supplierCoordinate'=>[]];}publicfunctioncreate(array$data):bool{$created=$this->table($this->tableName)->insert($data);return$created>0?true:false;}}
Enter fullscreen modeExit fullscreen mode

You can look atthc-fck core of my PHP Framework about theFckin\core\db\Model. That's it!

😊 Sorry for wasting your time...

Top comments(3)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Some comments may only be visible to logged-in visitors.Sign in to view all comments.

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Software Freestyle Engineer (SFE) and Metaphor Storyteller, I like to create useless tools and apps.
  • Location
    Tegal City, Indonesia
  • Education
    Street Community Programmer (SCP)
  • Pronouns
    l
  • Work
    Software Freestyle Engineer (Freestyler)
  • Joined

More fromImam Ali Mustofa

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp