Welcome Guest, Not a member yet? Register   Sign In
Immediate server side validation with Codeigniter HTMX and Bootstrap
#1

(This post was last modified: 06-18-2023, 04:41 AM by dgvirtual.)

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:

[Image: 4451]

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.
==

Donatas G.
Reply
#2

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

Donatas G.
Reply
#3

(This post was last modified: 06-19-2023, 06:04 AM by FabriceL.)

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.

[Image: Screenshot-24.png]
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
                    
]);
                
Reply




Theme © iAndrew 2016 - Forum software by © MyBB