Skip to content

Conversation

@derekmd
Copy link
Contributor

@derekmd derekmd commented Nov 1, 2016

Support Laravel 5.2 * for non-keyed PHP array inputs

e.g.,

  • Rule ['items.*.name' => 'required'] will validate:
    • DOM selector: '[name="items\\[\\]\\[name\\]"]'
    • DOM attribute: name="items[][name]"
  • Rule ['item_name.*' => 'required'] will validate:
    • DOM selector: '[name="item_name\\[\\]"]'
    • DOM attribute: name="items_name[]"

Laravel 5.2's Validator::make() immediately strips wildcard rules (since JsValidator's instantiation never includes data) so they must be tracked separately in the DelegatedValidator class.

Named PHP array key inputs not yet supported

Unfortunately named associative array keys are not yet supported in JavaScript as:

  • The jQuery Validation plugin requires exact form name="" attribute values when defining rules and custom messages.
  • $(document).ready() best-guess attempts to apply rules to name="" attributes on the page are made redundant by dynamically-added DOM elements that are the typical use case of wildcard rules.
    • Be it new table rows or Ajax resulting re-renders.
function discoverWildcardNames(wildcards) {
    var inputNames = $(':input')
        .map(function (i, el) {
            return el.getAttribute('name');
        })
        .toArray();

    var matches = wildcards
        .map(function (attribute) {
            return attribute
                .split('.')
                .map(function(key, i) {
                    if (i === 0) {
                        return key.replace('[', '\\[');
                    } else if (key === '*') {
                        return '\\[[^\\\\]*\\]';
                    } else {
                        return '\\[' +
                            key.replace('[', '\\[').replace(']', '\\]') +
                            '\\]';
                    }
                })
                .join('');
        })
        .map(function (pattern) {
            return new RegExp('^' + pattern + '$');
        })
        .map(function (regex) {
            return inputNames
                .slice(0)
                .filter(function (name) {
                    return regex.test(name);
                });
        });

    var nameMatches = {};
    for (var i in wildcards) {
        nameMatches[wildcards[i]] = matches[i];
    }

    return nameMatches;
}

// Example array validation from https://laravel.com/docs/5.3/validation#validating-arrays
discoverWildcardNames([
    'person.*.email',
    'person.*.first_name'
]);
  • New DOM elements <input>, <textarea>, or <select> inserted dynamically to the DOM tree will not be included for form validation.
  • The default Bootstrap view would have to be refactored to support such a callback when defining $().validate({rules: {}}).

If there's enough demand, I could try such a hack... because it would be a hack.

@derekmd derekmd changed the title Support 0-indexed arrays for >= Laravel 5.2 validation wildcard rules Support 0-indexed arrays for 5.2 validation wildcard rules Nov 1, 2016
@derekmd
Copy link
Contributor Author

derekmd commented Dec 4, 2016

Hey Albert, I just saw you'd jumped back onto this package today to update it for Laravel 5.3+. I also just noticed there's been some array validation wildcard code dating back to March not available in the published Composer/Packagist release. I assume this is for required_with, etc. rules that form dependencies between inputs and use PHP associative array keys for matching?

Using your latest master commit I tried an attribute name wildcard validation test:

{!!
    JsValidator::make([
        'user.*.first_name' => 'required|min:2',
        'user.*.last_name' => 'required',
        'user.*.email' => 'required|email',
    ])
!!}

This will output JavaScript:

jQuery(document).ready(function(){
    $("form").validate({
        errorElement: 'span',
        errorClass: 'help-block error-help-block',

        errorPlacement: function(error, element) {
            if (element.parent('.input-group').length ||
                element.prop('type') === 'checkbox' || element.prop('type') === 'radio') {
                error.insertAfter(element.parent());
                // else just place the validation message immediatly after the input
            } else {
                error.insertAfter(element);
            }
        },
        highlight: function(element) {
            $(element).closest('.form-group').removeClass('has-success').addClass('has-error'); // add the Bootstrap error class to the control group
        },

        /*
         // Uncomment this to mark as validated non required fields
         unhighlight: function(element) {
         $(element).closest('.form-group').removeClass('has-error').addClass('has-success');
         },
         */
        success: function(element) {
            $(element).closest('.form-group').removeClass('has-error').addClass('has-success'); // remove the Boostrap error class from the control group
        },

        focusInvalid: false, // do not focus the last invalid input
        
        rules: []
    })
})

Notably the rules prop is an empty array.

As my pull request shows in code comments, Mohamed Said's wildcard rewrite of the validator strips rules for any attribute name with '*'. Specifically that change is in Illuminate\Validation\Validator@explodeRules(). So those validation rules never make it through to the view.

With your arrayRules() function ( https://github.com/proengsoft/laravel-jsvalidation/blob/master/public/js/jsvalidation.js#L2153 ) or my above <input name=""> JS selector filtering, you could match the non-keyed attribute (e.g., user[][first_name]) to any keyed DOM node (user[49059][first_name]). The former's rules could be accessed in the proengsoft/laravel-jsvalidation/resources/views/bootstrap.php using $validator['rules'] + $validator['wildcards'] injected into that view.

How custom FormRequest@messages() work into that, I'm not quite sure!

e.g.,
* ['items.*.name' => 'required'] will validate DOM selector
   $('[name="items\\[\\]\\[name\\]"]') for name="items[][name]"
* ['item_name.*'] will validate DOM selector
   $('[name="item_name\\[\\]"]') for name="items_name[]"

Laravel 5.2's Validator::make() immediately strips wildcard rules so they
must be tracked separately in the DelegatedValidator class.

Unfortunately named associative array keys are not supported in
JavaScript as:

* jQuery Validation plugin requires exact form name="" attribute values
   when defining rules and custom messages.
* Best guess attempts to apply rules to name="" attributes on the
   page are made redundant by dynamically-added table rows that are
   the typical use case of wildcard rules.
   * e.g., For PHP rule  'items.*.name', this jQuery selector could guess
      a named key <input name="items[495][name]">:

      $('[name^="items\\["][name$="\\]\\[name\\]"]').map(function (i, el) {
          return el.name;
      });
   * This could obviously return false-positive DOM nodes to include in
      form validation. e.g., name="[items][][nested][name]"
   * New DOM <input>, <textarea>, or <select> inserted to the DOM
      tree will not be included for form validation.
   * The default Bootstrap view would have to be refactored to support
      such a callback.
@torrentalle
Copy link
Member

Hi @derekmd thanks for your contribution.

I was testing your branch and working on messages support. I would like to discuss about some changes to evict changes to resources/views/bootstrap.php file.

Can you change the merge branch of this PR to array-fields branch? There we will can make changes safety.

@derekmd derekmd changed the base branch from master to array-fields December 6, 2016 02:15
@torrentalle torrentalle merged commit 6e2c93f into proengsoft:array-fields Dec 6, 2016
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

Successfully merging this pull request may close these issues.

2 participants