Create a Dynamic Modal Using PHP and HTMX

· darkterminal's blog

Hello Punk! I am again... sorry for that.

The follow up question that come up in my invisible head about this journal:

1 Endpoint Do 5 Things for HTMX DataTable

is... 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!.

Hmmm... my question is super interesting. I would not compare with other approach (I mean language or framework), I just want to share what I am doing before (Yes! my fck PHP Framework)


Technically, I am so proud of myself and I need to prepare for something interesting and wasting my time! 😏 Oh yeah beibeh


# What's The Plan?!

High Level Plan Screenshot from htmx modal custom - example

# The Meme

20 Year Old

# The Hypertext Markup Language of The Modal

This not fully HTML (I see it bro!). Also have a PHP tag inside (Seriously!?) that's part of my fck PHP Framework (Fck'it bro!).

# The Create Button

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

The Create Button

# The Modal Itself!

 1<?php
 2// Filename: /home/darkterminal/workspaces/fck-htmx/views/components/suppliers/modals/modal-create.php
 3
 4use App\config\helpers\Icons;
 5use App\config\helpers\Utils;
 6
 7?>
 8<div id="modal" _="on closeModal wait 250ms add .closing then wait for animationend then remove me">
 9    <div class="modal-underlay" _="on click trigger closeModal"></div>
10    <div class="w-7/12 min-h-80 max-h-[40em] modal-content">
11        <div class="flex justify-between p-2 mb-3 border-b-2">
12            <h2 class="text-lg font-bold">Create New Supplier</h2>
13            <button class="btn btn-sm btn-ghost btn-circle" _="on click trigger closeModal"><?= Icons::use('XMark') ?></button>
14        </div>
15        <form class="grid grid-cols-2 gap-3 p-3 w-full" method="post" hx-post="<?= base_url('suppliers/create') ?>" hx-target="#modal" hx-swap="outerHTML" autocomplete="off">
16            <label class="w-full form-control">
17                <div class="label">
18                    <span class="label-text">Supplier Name</span>
19                </div>
20                <input type="text" name="supplierName" placeholder="Supplier name" class="w-full input input-sm input-bordered <?= !empty(Utils::getValidationError($errors, 'supplierName')) ? 'input-error' : '' ?>" />
21                <div class="label">
22                    <?= Utils::getValidationError($errors, 'supplierName') ?>
23                </div>
24            </label>
25            <label class="w-full form-control">
26                <div class="label">
27                    <span class="label-text">Supplier Company Name</span>
28                </div>
29                <input type="text" name="supplierCompanyName" placeholder="Supplier company name" class="w-full input input-sm input-bordered <?= !empty(Utils::getValidationError($errors, 'supplierCompanyName')) ? 'input-error' : '' ?>" />
30                <div class="label">
31                    <?= Utils::getValidationError($errors, 'supplierCompanyName') ?>
32                </div>
33            </label>
34            <label class="w-full form-control">
35                <div class="label">
36                    <span class="label-text">Supplier Phone Number</span>
37                </div>
38                <input type="text" name="supplierPhoneNumber" placeholder="Supplier phone number" class="w-full input input-sm input-bordered <?= !empty(Utils::getValidationError($errors, 'supplierPhoneNumber')) ? 'input-error' : '' ?>" />
39                <div class="label">
40                    <?= Utils::getValidationError($errors, 'supplierPhoneNumber') ?>
41                </div>
42            </label>
43            <label class="w-full form-control">
44                <div class="label">
45                    <span class="label-text">Supplier Email</span>
46                </div>
47                <input type="text" name="supplierEmail" placeholder="Supplier email address" class="w-full input input-sm input-bordered <?= !empty(Utils::getValidationError($errors, 'supplierEmail')) ? 'input-error' : '' ?>" />
48                <div class="label">
49                    <?= Utils::getValidationError($errors, 'supplierEmail') ?>
50                </div>
51            </label>
52            <label class="w-full form-control">
53                <div class="label">
54                    <span class="label-text">Latitude Coordinate</span>
55                </div>
56                <input type="text" name="latitude" placeholder="Latitude" class="w-full input input-sm input-bordered" />
57            </label>
58            <label class="w-full form-control">
59                <div class="label">
60                    <span class="label-text">Longitude Coordinate</span>
61                </div>
62                <input type="text" name="longitude" placeholder="Longitude" class="w-full input input-sm input-bordered" />
63            </label>
64            <label class="col-span-2 w-full form-control">
65                <div class="label">
66                    <span class="label-text">Address</span>
67                </div>
68                <textarea class="h-24 textarea textarea-bordered <?= !empty(Utils::getValidationError($errors, 'supplierAddress')) ? 'input-error' : '' ?>" name="supplierAddress" placeholder="Supplier Address"></textarea>
69                <div class="label">
70                    <?= Utils::getValidationError($errors, 'supplierAddress') ?>
71                </div>
72            </label>
73            <div class="col-span-2">
74                <button class="btn btn-sm btn-block btn-neutral" type="submit">Save</button>
75            </div>
76        </form>
77    </div>
78</div>
79

The Modal Itself!

Wait... what is this?!

1<div id="modal" _="on closeModal wait 250ms add .closing then wait for animationend then remove me">
2    <div class="modal-underlay" _="on click trigger closeModal"></div>

and this

1<button class="btn btn-sm btn-ghost btn-circle" _="on click trigger closeModal"><?= Icons::use('XMark') ?></button>

Oh sorry, I am not mention it before. If you don't know what is that, that's the _hyperscript.

_hyperscript little introduction

When the XMark button is clicked then the button will be send event called closeModal, where the closeModal event is received at:

1<div id="modal" _="on closeModal wait 250ms add .closing then wait for animationend then remove me">

So whenever this event is (on) triggered they will wait 250ms and add-ing a .closing class in that element then wait for animationend then remove me me mean that element itself.

Huuuuuh... how good I am explaining!? Dang bro... that sound weird for me!

# First Milestone

First Milestone

The first milestone is to create this and display it. In this moment is not doing anything. But after we create a routing for that modal request and adding some CSS (that I copy and paste in the htmx docs example)


# The CSS (copy and paste)

Source: https://htmx.org/examples/modal-custom/

 1/***** MODAL DIALOG ****/
 2#modal {
 3	/* Underlay covers entire screen. */
 4	position: fixed;
 5	top:0px;
 6	bottom: 0px;
 7	left:0px;
 8	right:0px;
 9	background-color:rgba(0,0,0,0.5);
10	z-index:1000;
11
12	/* Flexbox centers the .modal-content vertically and horizontally */
13	display:flex;
14	flex-direction:column;
15	align-items:center;
16
17	/* Animate when opening */
18	animation-name: fadeIn;
19	animation-duration:150ms;
20	animation-timing-function: ease;
21}
22
23#modal > .modal-underlay {
24	/* underlay takes up the entire viewport. This is only
25	required if you want to click to dismiss the popup */
26	position: absolute;
27	z-index: -1;
28	top:0px;
29	bottom:0px;
30	left: 0px;
31	right: 0px;
32}
33
34#modal > .modal-content {
35	/* Position visible dialog near the top of the window */
36	margin-top:10vh;
37
38	/* Sizing for visible dialog */
39	width:80%;
40	max-width:600px;
41
42	/* Display properties for visible dialog*/
43	border:solid 1px #999;
44	border-radius:8px;
45	box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.3);
46	background-color:white;
47	padding:20px;
48
49	/* Animate when opening */
50	animation-name:zoomIn;
51	animation-duration:150ms;
52	animation-timing-function: ease;
53}
54
55#modal.closing {
56	/* Animate when closing */
57	animation-name: fadeOut;
58	animation-duration:150ms;
59	animation-timing-function: ease;
60}
61
62#modal.closing > .modal-content {
63	/* Animate when closing */
64	animation-name: zoomOut;
65	animation-duration:150ms;
66	animation-timing-function: ease;
67}
68
69@keyframes fadeIn {
70	0% {opacity: 0;}
71	100% {opacity: 1;}
72} 
73
74@keyframes fadeOut {
75	0% {opacity: 1;}
76	100% {opacity: 0;}
77} 
78
79@keyframes zoomIn {
80	0% {transform: scale(0.9);}
81	100% {transform: scale(1);}
82} 
83
84@keyframes zoomOut {
85	0% {transform: scale(1);}
86	100% {transform: scale(0.9);}
87}

# The Routing #1 GET

1<?php
2// Filename: /home/darkterminal/workspaces/fck-htmx/routes/web.php
3use Fckin\core\Application;
4
5/** @var Application $app  */
6
7$app->router->get('suppliers/create', 'Suppliers@create');

This route is corresponding to get the modal content from the backend (Yes! PHP). then display in the body of the page as mentioned in this button and swap it beforeend:

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

# The Controller #1 GET

 1<?php
 2// Filename: /home/darkterminal/workspaces/fck-htmx/controllers/Suppliers.php
 3
 4namespace App\controllers;
 5
 6use App\config\helpers\Utils;
 7use App\models\Suppliers as ModelsSuppliers;
 8use Fckin\core\Controller;
 9use Fckin\core\Request;
10use Fckin\core\Response;
11
12class Suppliers extends Controller
13{
14    protected $suppliers;
15
16    public function __construct()
17    {
18        $response = new Response();
19        if (!isAuthenticate()) {
20            $response->setStatusCode(401);
21            exit();
22        }
23        $this->suppliers = new ModelsSuppliers();
24    }
25
26public function create(Request $request)
27    {
28        $params = []; // <-- I will do it something here when make POST request
29
30        return Utils::addComponent('suppliers/modals/modal-create', $params);
31    }
32}

That's it! Here I am...

First Milestone

If anything goes wrong or not working! It's my fault! not me. (Are you confuse? Yes, me too...)


See ya in the next stone! 👋 I am so tired!!! Hope you mad of me... thank you very much, I appreciate that...