Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Client Side Validation not working with Custom ValidationRule #1000

Open
kennethenglisch opened this issue Feb 20, 2024 · 2 comments
Open

Client Side Validation not working with Custom ValidationRule #1000

kennethenglisch opened this issue Feb 20, 2024 · 2 comments

Comments

@kennethenglisch
Copy link

Subject of the issue

Hello, I'm using your package to validate the client side of the user inputs in my laravel project.
Now that I'm using select-inputs with the attribute 'multiple', I got into problems with the Client Side Validation.
I need a custom ValidationRule to do my checks if any of the chosen inputs inside of the select are restricted to each other.
The Custom ValidationRule works as intended on the server side.

I already tried to use following attributes for the rules:

  • 'license-types'
  • 'license-types[]'
  • 'license-types.*'

Your environment

  • version of this package: 4.8.1
  • version of Laravel: 10.44.0

Steps to reproduce

Select Input as HTML:

<select class="form-select license-types-select" id="license-types" name="license-types[]" data-placeholder="Wähle deine Lizenz oder Lizenzen..." multiple="multiple">
    <option value="1">Obmann</option>
    <option value="2">Stellvertretender Obmann</option>
    <option value="3">Hauptschiedsrichter</option>
    <option value="4">Linienschiedsrichter</option>
</select>

Profile Request Class:

<?php

namespace Modules\Profile\Http\Requests;

use App\Components\ValidatorRules\PhoneValidatorRules;
use App\Http\Requests\FormRequest;
use Modules\Profile\Rules\LicenseTypeRequestRule;
use Modules\Profile\Rules\TeamRequestRule;

class ProfileRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
     */
    public function rules(): array
    {
        return [
            // contact information and address
            'birth-date'        => ['required', 'date'],
            'mobile'            => PhoneValidatorRules::GetRulesByConfig(true),
            'primary-email'     => ['required', 'email'],
            'secondary-email'   => ['email'],
            'address-street'    => ['required', 'string', 'max:50'],
            'address-number'    => ['required', 'string', 'max:10', 'regex:/^[1-9]\d*(?:[ -]?(?:[a-zA-Z]+|[1-9]\d*))?$/'],
            'address-zipcode'   => ['required', 'string', 'min:5', 'max:5', 'regex:/^[0-9]{5}$/'], // for now only germany since we don't have a country field here yet
            'address-city'      => ['required', 'string', 'max:45'],

            // referee information
            'license-types'     => ['required', new LicenseTypeRequestRule()],
            'license-number'    => ['required', 'regex:' . config('app.params.licenseNumberRegex')],
            'team'              => ['required', new TeamRequestRule(5)],
        ];
    }

    public function attributes()
    {
        return [
            // contact information and address
            'birth-date'        => t('profile::edit.birth-date-label'),
            'mobile'            => t('profile::edit.mobile-label'),
            'primary-email'     => t('profile::edit.primary-email-label'),
            'secondary-email'   => t('profile::edit.secondary-email-label'),
            'address-street'    => t('profile::edit.address-street-label'),
            'address-number'    => t('profile::edit.address-number-label'),
            'address-zipcode'   => t('profile::edit.address-zipcode-label'),
            'address-city'      => t('profile::edit.address-city-label'),

            // referee information
            'license-types'     => tc('profile::edit.license-type-label'),
            'license-number'    => t('profile::edit.license-number-label'),
            'team'              => t('profile::edit.team-label'),
        ];
    }
}

LicenseTypeRequestRule Class: (I used ValidationRule instead of Rule, because Rule is deprecated)

<?php

namespace Modules\Profile\Rules;

use App\Models\LicenseType;
use App\Models\LicenseTypeRestrictions;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class LicenseTypeRequestRule implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if(!is_array($value)) {
            $fail(t('profile::edit.license-types-invalid', ['attribute' => tc('profile::edit.license-type-label')]));
            return;
        }

        $license_types = LicenseType::all();

        // make sure to check from license_types_id to restricted_license_types_id and the other way around
        $license_types_restrictions = LicenseTypeRestrictions::where('deleted', false)->get()->toArray();

        // check if chosen licenses are valid
        foreach($value as $chosenLicense) {
            if(!$license_types->contains($chosenLicense)) {
                $fail(t('profile::edit.license-types-invalid', ['attribute' => tc('profile::edit.license-type-label')]));
                return;
            }
        }

        if(sizeof($value) > 1) {
            // check if chosen licenses are restricted
            foreach($license_types_restrictions as $restriction) {
                // if any of the chosen licenses is a license_types_id and another a restricted_license_types_id
                if(in_array($restriction['license_types_id'], $value) && in_array($restriction['restricted_license_types_id'], $value)){
                    $fail(t('profile::edit.license-types-restricted', ['attribute' => tc('profile::edit.license-type-label')]));
                    return;
                }
            }
        }
    }
}

License Types from the db, exported as php array:

$lst_license_types = array(
	array(
		"id" => 1,
		"name" => "Obmann",
		"description" => "Obmann des LEV",
		"created_at" => "2023-09-04 19:18:48",
		"updated_at" => "2023-09-04 19:18:48",
	),
	array(
		"id" => 2,
		"name" => "Stellvertretender Obmann",
		"description" => "Stellvertretender Obmann des LEV",
		"created_at" => "2023-09-04 19:18:48",
		"updated_at" => "2023-09-04 19:18:48",
	),
	array(
		"id" => 3,
		"name" => "Hauptschiedsrichter",
		"description" => "Hauptschiedsrichter",
		"created_at" => "2023-09-04 19:18:48",
		"updated_at" => "2023-09-04 19:18:48",
	),
	array(
		"id" => 4,
		"name" => "Linienschiedsrichter",
		"description" => "Linienschiedsrichter",
		"created_at" => "2023-09-04 19:18:48",
		"updated_at" => "2023-09-04 19:18:48",
	),
);

License Types Restrictions from the db, exported as php array:

$lst_license_types_restrictions = array(
	array(
		"id" => 1,
		"license_types_id" => 1,
		"restricted_license_types_id" => 2,
		"deleted" => 0,
		"created_at" => "2024-02-17 13:07:43",
		"updated_at" => "2024-02-17 13:07:43",
	),
);

Expected behaviour

When the user selects the license type 'Obmann' and 'Stellvertretender Obmann' in the multiple-select, the error should be shown, because these types are restricted and can't be selected together. The user should not be able to submit the form.

Actual behaviour

When the user selects the license type 'Obmann' and 'Stellvertretender Obmann' in the multiple-select, the validation passes and the user can submit the form. After the submission the server side validation throws an error.

@bytestream
Copy link
Collaborator

I would expect it to treat ValidationRule as a remote rule and so you'll see it fire an AJAX request in the background. You'll have to debug why it's passing on the AJAX request using xdebug.

You can switch to using #505 as a compromise.

@kennethenglisch
Copy link
Author

It is treated as a remote rule.

I changed everthing according to #505 but it didn't work for me either.

I also changed the bootstrap5.php for adjusting error placement as well as indicating required fields.

<?php

use Illuminate\Support\Facades\Route;

$excludedRoutes = [
    'login',
    'password.forgot'
];

?>
<script type="module">
    jQuery(document).ready(function () {

        $("<?= $validator['selector']; ?>").each(function () {
            <?php
            if(config('app.params.indicateRequiredFields', false) && !in_array(Route::current()->getName(), $excludedRoutes) ) {
                foreach($validator['rules'] as $attribute => $rules) {

                    // do it always for password confirmation
                    if($attribute == 'password_confirmation'){
                        ?>
                            $('label[for="<?= $attribute ?>"]').addClass("required");
                        <?php
                        continue;
                    }

                    foreach ($rules['laravelValidation'] as $rule) {
                        if(in_array('required', $rule)) {
                            ?>
                                $('label[for="<?= $attribute ?>"]').addClass("required");
                            <?php
                        }
                    }
                    ?>
            <?php
                }
            }
            ?>

            $(this).validate({
                errorElement: 'div',
                errorClass: 'invalid-feedback',

                errorPlacement: function (error, element) {
                    const id = '#' + error.attr('id');
                    if(element.find(id).length > 0 || element.parent().find(id).length > 0) {
                        // we already have an error text div, so we skip and don't add a new one
                        return;
                    }

                    if (element.parent('.input-group').length ||
                        element.prop('type') === 'checkbox' || element.prop('type') === 'radio') {
                        error.insertAfter(element.parent());
                        // else just place the validation message immediately after the input
                    } else if(element[0].classList.contains('select2-hidden-accessible')) {
                        error.insertAfter(element.parent()[0].lastElementChild);
                    } else {
                        error.insertAfter(element);
                    }
                },
                highlight: function (element) {
                    $(element).removeClass('is-valid').addClass('is-invalid'); // add the Bootstrap error class to the control group
                },

                <?php if (isset($validator['ignore']) && is_string($validator['ignore'])): ?>

                ignore: "<?= $validator['ignore']; ?>",
                <?php endif; ?>


                unhighlight: function (element) {
                    $(element).removeClass('is-invalid').addClass('is-valid');
                },

                success: function (element) {
                    $(element).removeClass('is-invalid').addClass('is-valid'); // remove the Boostrap error class from the control group
                },

                focusInvalid: true,
                <?php if (Config::get('jsvalidation.focus_on_error')): ?>
                invalidHandler: function (form, validator) {

                    if (!validator.numberOfInvalids())
                        return;

                    $('html, body').animate({
                        scrollTop: $(validator.errorList[0].element).offset().top
                    }, <?= Config::get('jsvalidation.duration_animate') ?>);

                },
                <?php endif; ?>

                rules: <?= json_encode($validator['rules']); ?>
            });
        });
    });
</script>

Maybe I need to debug it and have a look at xdebug.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants