Immediate server side validation with Codeigniter HTMX and Bootstrap - dgvirtual - 06-18-2023
I am building a form that would provide immediate validation done server-side for each field with the help of HTMX, Codeigniter-HTMX and Bootstrap 5. I would like suggestions from more knowledgeable people on how to improve it perhaps / make more consise.
So, here is what it looks like:
once you leave a field (on blur), the form field sends htmx request to the field validation controller method which returns a fragment of the field updated according to the validation results. So the user gets almost-immediate validation of entered data.
On the server-side, thanks to the wonderful codeigniter-htmx library of @michalsn, the view is stored only once, and upon field validation request only it's fragment is returned. But when the form is submitted, the whole view is returned.
The shortcomings:
- User can submit the form at any time, even if it is invalid (I have not figured out how to prevent that).
- An instruction to disable the submit button while field validation request is in progress prevent request clash, but that means, that when you enter the last value and click immediately the submit button, it does not work; so you have to click it again after validation request is finished. This can be prevented with hx-sync but only after I have transformed the form to be fully managed by htmx (now it is a htmx-boosted form only).
So to use / test the form you need:
Bootstrap 5 CSS/JS, htmx JS, and HTMX extension disable-element JS files in your layout view:
Code: <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<script src="https://unpkg.com/[email protected]"></script>
<script src="https://unpkg.com/htmx.org/dist/ext/disable-element.js"></script>
Then this is my controller:
PHP Code: <?php
namespace App\Controllers;
class Fragment extends BaseController {
public function create() { $this->data['msg'] = $this->session->getFlashdata('msg'); $this->data['validation'] = service('validation'); helper('form'); $this->data['title'] = 'Immediate server-side validation using fragments';
return view('fragment_form_view', $this->data); }
public function save() { $validation = service('validation'); helper('form'); $validation->setRules($this->returnRules());
if (!$validation->withRequest($this->request)->run()) { $this->session->setFlashdata('msg', 'Errors in form!'); return redirect()->to('fragment/create')->withInput(); }
/** * logic to insert/update the data goes here */
$this->session->setFlashdata('msg', 'Success!');
return redirect()->to('fragment/create')->withInput(); }
private function returnRules() { return [ 'first_name' => ['label' => 'First Name', 'rules' => 'required|min_length[5]'], 'last_name' => ['label' => 'Last Name', 'rules' => 'required|min_length[5]'], 'color' => ['label' => 'Color', 'rules' => 'required|in_list[blue,yelow,green]'], ]; }
public function validateField(string $fieldName): string { $validation = service('validation'); helper('form');
$validation->setRules($this->returnRules($fieldName)); $validation->withRequest($this->request)->run();
if ($validation->hasError($fieldName)) { $this->data['errors'] = $validation->getErrors(); $this->data['status'][$fieldName] = ' is-invalid'; } else { $this->data['status'][$fieldName] = ' is-valid'; }
$this->data['values'][$fieldName] = $this->request->getVar($fieldName);
$this->data['validation'] = $validation;
return view_fragment('fragment_form_view', $fieldName, $this->data); } }
And this is my view file (that also needs the layout, as mentioned above):
PHP Code: <?= $this->extend('common/default_layout') ?>
<?= $this->section('content') ?>
<?php if (!empty($msg)) : ?> <div class="alert alert-secondary alert-dismissible fade show" role="alert"> <p><?= $msg ?></p> </div> <?php endif; ?>
<h3 class="mt-4"><?php echo $title; ?></h3>
<ol class="breadcrumb mb-4"> <li class="breadcrumb-item">Fragments</li> <li class="breadcrumb-item active"><?php echo $title; ?></li> </ol>
<div class="container" style="max-width:500px;">
<form hx-boost="true" action="<?= site_url('fragment/save') ?>" method="post"> <?= csrf_field() ?>
<div class="row"> <?php $fieldName = 'first_name'; ?> <div id="<?=$fieldName?>" class="col-12"> <?php $this->fragment($fieldName); ?> <div class="mb-3"> <label class="form-label" for="<?=$fieldName?>">First Name</label> <input hx-trigger="blur" hx-target="#<?=$fieldName?>" hx-get="<?= site_url('fragment/validateField/'. $fieldName) ?>" hx-ext="disable-element" hx-disable-element="#submit" hx-sync="closest form:abort" type="text" class="form-control<?php if (!empty(validation_show_error($fieldName))) echo ' is-invalid'; elseif (validation_errors()) echo ' is-valid'; ?>" name="<?=$fieldName?>" value="<?= $values[$fieldName] ?? old($fieldName) ?>" > <?= validation_show_error($fieldName) ?> </div> <?php $this->endFragment(); ?> </div>
<?php $fieldName = 'last_name'; ?> <div id="<?=$fieldName?>" class="col-12"> <?php $this->fragment($fieldName); ?> <div class="mb-3"> <label class="form-label" for="<?=$fieldName?>">Last Name</label> <input hx-trigger="blur" <?php /*keyup delay:500ms*/?> hx-target="#<?=$fieldName?>" hx-ext="disable-element" hx-disable-element="#submit" hx-sync="closest form:abort" hx-get="<?= site_url('fragment/validateField/'. $fieldName) ?>" type="text" class="form-control<?php if (!empty(validation_show_error($fieldName))) echo ' is-invalid'; elseif (validation_errors()) echo ' is-valid'; ?>" name="<?=$fieldName?>" value="<?= $values[$fieldName] ?? old($fieldName) ?>" > <?= validation_show_error($fieldName) ?> </div> <?php $this->endFragment(); ?> </div>
<?php $fieldName = 'color'; ?> <div id="<?=$fieldName?>" class="col-12"> <?php $this->fragment($fieldName); ?> <div class="mb-3"> <label class="form-label" for="<?=$fieldName?>">Color</label> <select hx-trigger="blur" hx-target="#<?=$fieldName?>" hx-ext="disable-element" hx-disable-element="#submit" hx-get="<?= site_url('fragment/validateField/'. $fieldName) ?>" type="text" class="form-select<?php if (!empty(validation_show_error($fieldName))) echo ' is-invalid'; elseif (validation_errors()) echo ' is-valid'; ?>" name="<?=$fieldName?>" > <option value="">- select one -</option> <option value="blue" <?= (($values[$fieldName] ?? old($fieldName)) == 'blue') ? 'selected' : '' ?>>Blue</option> <option value="yelow" <?= (($values[$fieldName] ?? old($fieldName)) == 'yelow') ? 'selected' : '' ?>>Yelow</option> <option value="red" <?= (($values[$fieldName] ?? old($fieldName)) == 'red') ? 'selected' : '' ?>>Red</option> <option value="green" <?= (($values[$fieldName] ?? old($fieldName)) == 'green') ? 'selected' : '' ?>>Green</option> </select> <?= validation_show_error($fieldName) ?> </div> <?php $this->endFragment(); ?> </div>
<div class="col-12"> <div class="mb-3"> <button id="submit" type="submit" class="btn btn-success">Save</button> </div> </div> </div>
</form> </div>
<?= $this->endSection() ?>
I would be really grateful for any improvements and suggestions how to make the code more concise.
RE: Immediate server side validation with Codeigniter HTMX and Bootstrap - dgvirtual - 06-18-2023
I updated the code a little and put it on a github repo: now it is easy to install and see it in action:
https://github.com/dgvirtual/ci-htmx-form
See README.md file for install instructions
RE: Immediate server side validation with Codeigniter HTMX and Bootstrap - FabriceL - 06-19-2023
Hello @dgvirtual ,
I'm working on a project with which I use HTMX/tailwindcss.
I don't use fragments like you do, but CI4 view.
Form :
PHP Code: <?= form_open(route_to('customer-information', $customer->getIdentifierUUID()), [ 'id' => 'kt_customers_form_information', 'hx-post' => route_to('customer-information', $customer->getIdentifierUUID()), 'hx-target' => '#addaddress', 'hx-ext' => "loading-states, event-header", 'novalidate' => false, 'data-loading-target' => "#loadingaddaddress", 'data-loading-class-remove' => "hidden" ]); ?> <?= '' //csrf_field() ?> <input type="hidden" name="section" value="adresse" />
<?= $this->include('Btw\Customer\Views\Admin\cells\form_cell_form_addresses'); ?>
<?= form_close(); ?>
Vue :
PHP Code: view_cell('Btw\Core\Cells\InputCell::renderList', [ 'type' => 'text', 'label' => lang('Form.general.alias'), 'name' => 'alias', 'value' => old('alias', $customer->alias), 'lang' => false, ]);
Controller :
PHP Code: $data = $this->request->getPost(); $validation = service('validation');
$validation->setRules([ 'email' => 'required|valid_email|unique_email[' . $customer->id . ']', 'lastname' => 'permit_empty|string|min_length[3]', 'firstname' => 'permit_empty|string|min_length[3]', 'type' => 'required'
]);
if (!$validation->run($data)) { $this->response->triggerClientEvent('showMessage', ['type' => 'error', 'content' => $validation->getErrors()]); return view($this->viewPrefix . 'cells\form_cell_information', [ 'customer' => $customer, 'validation' => $validation ]); }
|