michalsn / codeigniter-htmx

HTMX helper library for CodeIgniter 4 framework
https://michalsn.github.io/codeigniter-htmx/
MIT License
75 stars 15 forks source link

view_fragment returns whole page: what am I doing wrong? #38

Closed dgvirtual closed 1 year ago

dgvirtual commented 1 year ago

I am attempting to implement form field validation that would mimic client-side validation, but view_fragment is refusing to return only a fragment of the view :(

Here is my controller:

<?php

namespace App\Controllers;

use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Psr\Log\LoggerInterface;

class Fragment extends BaseController
{

    private $validation;

    public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger)
    {
        // Do Not Edit This Line
        parent::initController($request, $response, $logger);
        $this->validation =  \Config\Services::validation();
        helper('form');
    }

    public function create()
    {

        $this->data['title'] = 'Test with fragments';
        $this->data['validation'] = $this->validation;

        $this->data['includes'] = ['htmx'];
        return view('fragment_form_view', $this->data);
    }

    private function returnRules()
    {
        return [
            'first_name' => ['label' => 'Name', 'rules' => 'required|min_length[5]'],
            'last_name' => ['label' => 'Surname', 'rules' => 'required|min_length[5]'],
        ];
    }

    public function validateField(string $fieldName): string
    {

        $this->validation->setRules($this->returnRules($fieldName));
        $this->validation->withRequest($this->request)->run();

        if ($this->validation->hasError($fieldName)) {
            $this->data['errors'] = $this->validation->getErrors();
        }

        $this->data[$fieldName] = $this->request->getPost($fieldName);

        $this->data['title'] = '';
        $this->data['validation'] = $this->validation;

        return view_fragment('fragment_form_view', $fieldName, $this->data);
    }
}

And this is my view file fragment_form_view.php:

<?= $this->extend('common/default_layout') ?>

<?= $this->section('content') ?>

<h1 class="mt-4"><?php echo $title; ?></h1>

<ol class="breadcrumb mb-4">
    <li class="breadcrumb-item">Fragmentai</li>
    <li class="breadcrumb-item active"><?php echo $title; ?></li>
</ol>

<div class="container" style="max-width:500px;">

    <form action="<?= site_url('fragment/save') ?>" method="post">

        <div class="row">
            <div id="first_name" class="col-12">
                <?php $this->fragment('first_name'); ?>
                <div class="mb-3">
                    <label class="form-label" for="first_name">Name</label>
                    <input hx-target="#first_name" hx-post="<?= site_url('fragment/validateField/first_name') ?>" type="text" class="form-control" name="first_name" value="<?php old('first_name'); ?>">
                    <?= $validation->showError('first_name') ?>
                </div>
                <?php $this->endFragment(); ?>
            </div>

            <div id="last_name" class="col-12">
                <?php $this->fragment('last_name'); ?>
                <div class="mb-3">
                    <label class="form-label" for="last_name">Surname</label>
                    <input hx-target="#last_name" hx-post="<?= site_url('fragment/validateField/last_name') ?>" type="text" class="form-control" name="last_name" value="<?php old('last_name'); ?>">
                    <?= $validation->showError('last_name') ?>
                </div>
                <?php $this->endFragment(); ?>
            </div>

            <div class="col-12">
                <div class="mb-3">
                    <button type="submit" class="btn btn-success">Send</button>

                </div>
            </div>
        </div>

    </form>
</div>

<?= $this->endSection() ?>

They are messy and incomplete, but the code should return view fragment on blur of a form field. Instead, it returns the whole page (and messes up the display of the form. What am I doing wrong? Or is it a bug in the ci-htmx lib?

dgvirtual commented 1 year ago

I have tried making the view a simple view, that wouldnot extend a layout, but still the whole view is returned each time.

michalsn commented 1 year ago

This looks like a bug. Thanks for reporting it!

I'll take a look at it tomorrow.

dgvirtual commented 1 year ago

On further tests I notice that if I turn on csrf protection for forms in config/Filters.php, the get method still works as expected, but if I switch to post method, the old bug resurfaces; so, this code in view makes the view_fragment return the full view:

        <div id="last_name" class="col-12">
            <?php $this->fragment('last_name'); ?>
            <div class="mb-3">
                <label class="form-label" for="last_name">Pavardė</label>
                <input hx-target="#last_name" hx-post="<?= site_url('fragment/validateField/last_name') ?>" type="text" class="form-control" name="last_name" value="<?php old('last_name'); ?>">
                <?= $validation->showError('last_name') ?>
            </div>
            <?php $this->endFragment(); ?>
        </div>

My config values from Config/Security.php:

Security.txt

and the Config/Filters.php $globals array with csrf protection enabled:

/**
 * List of filter aliases that are always
 * applied before and after every request.
 *
 * @var array
 */
public $globals = [
    'before' => [
        'checkLogin' => [
            'except' => [
                'login',
                'register',
                'activate/*',
                'forgotpasswd',
                'resetpasswd/*',
            ]
            ], //just add like this
        // 'honeypot',
        'csrf',
    ],
    'after' => [
        'toolbar',
        // 'honeypot',
    ],
];

Anything else I need to attach/paste to help troubleshoot?

michalsn commented 1 year ago

So, you're now using v1.2.1?

I was trying to reproduce it but without luck. Are we still testing single-input validation only?

With CSRF enabled and your security settings everything works fine. What CodeIgniter version are you using? Is there anything else you have changed (maybe in your code)?

It seems like the only difference is checkLogin filter. Can you disable it for a moment and check again?

dgvirtual commented 1 year ago

Hi Michal, sorry it took some time to recheck and make sure I did not mix up anything. I have finally found the culprit: the code started to work fine when I added <?php echo csrf_field(); ?> at the start of the form...

Now I see that there is was a difference in the browser console: it is not the 'fragment/validateField/first_name' that was returning the full view; the request to 'fragment/validateField/first_name' is redirected (303?) to 'fragment/create', and that is why I get the full view... Here is the screenshot:

paveikslas

The missing csrf_field() must have been causing the form to be rejected and redirected back; add the ajax nature of the call and one gets a mess on the page...

The initial issue, though, was not caused by csrf, as at that time I did not have it enabled. Thanks for fixing it.

Sorry to have made you test this. I think we can close the bug report. Below, are the answers to your questions (just in case), that I wrote before understanding the problem.

So, you're now using v1.2.1?

Yes, freshly updated to dev-develop a85c93a; and latest Codeigniter, with php 8.1.8,

I was trying to reproduce it but without luck. Are we still testing single-input validation only?

yes, exactly the same code, same behaviour. I had it changed in the view file to <input hx-target="#first_name" hx-get=... instead of the code I submitted here above as an example with <input hx-target="#first_name" hx-post=...

It works fine with hx-get , returning only a fragment of code, and then with hx-post it starts returning whole view instead.

With CSRF enabled and your security settings everything works fine. What CodeIgniter version are you using? Is there anything else you have changed (maybe in your code)?

I am using the latest CI. And I have not modified CI in any way. On the other hand, I have not tested this controller on a fresh install of CI (mine is 2 years old, but updated), so I am not sure if any other settings in my app folder are not affecting the behaviour... I am also using htmx from

When I disable csrf (comment it out in the Config Filters->globals), the hx-post works fine. But when I enable it back, I get full views again.

It seems like the only difference is checkLogin filter. Can you disable it for a moment and check again?

Tried that, it does not change things.

michalsn commented 1 year ago

Okay, I'm glad you figured it out.

As for CSRF - you may encounter an error if you enable the regenerate option in the Security config file. If you are determined to enable this option, then hx-swap-oob feature may be useful. You can use it to replace the old csrf_field().

dgvirtual commented 1 year ago

I noticed I get error with regenerate option enabled, so I disabled it. Thanks for suggestion regarding hx-swap-oob; seems to me it would be an overkill with my type of application, but in case I ever make an app requiring maximum bulletproof security, I will keep that in mind. Thanks again for helping make the things work with codeigniter-htmx.