October: CMS Page Repeater Validation Failure

Created on 1 Mar 2019  路  36Comments  路  Source: octobercms/october

Description:

When adding _repeater_ field to CMS page / required validation doesn't work

Steps To Reproduce:

Validation throws required error even though data is present. This isn't limited to repeater fields.

Review Needed Bug help wanted

Most helpful comment

@bennothommo I will do some more testing over the weekend, it _was_ very late last night!

All 36 comments

@seanthepottingshed awesome thanks for the test-plugin example!

@w20k

Further to our Slack conversation, neither of the below approaches work:

$model = $widget->model;
$model->bindEvent('model.beforeValidate', function () use ($model) {
    $model->rules += [
        'test_repeater' => 'required',
    ];
});
$model = $widget->model;
$model->bindEvent('model.beforeValidate', function () use ($model) {
    $model->rules += [
        'viewBag[test_repeater]' => 'required',
    ];
});

@seanthepottingshed does updating the rules in beforeValidate ever work? How about doing it in beforeSave instead?

@LukeTowers

Good shout, that didn鈥檛 even occur to me. I鈥檒l give that a go this evening and let you you know.

@LukeTowers

Tried the following and neither versions work:

$model = $widget->model;
$model->bindEvent('model.beforeSave', function () use ($model) {
    $model->rules += [
        'test_repeater' => 'required',
    ];
});
$model = $widget->model;
$model->bindEvent('model.beforeSave', function () use ($model) {
    $model->rules += [
        'viewBag[test_repeater]' => 'required',
    ];
});

So whole thing:

public function boot()
{
    if (\App::runningInBackend()) {
        \Event::listen('backend.form.extendFields', function ($widget) {
            if (!$widget->model instanceof \Cms\Classes\Page) {
                return;
            }
            if ($widget->isNested || $widget->model->settings['url'] !== '/') {
                return;
            }
            $widget->addFields([
                'viewBag[test_field]'     => [
                    'label'       => 'Test Field',
                    'placeholder' => 'Enter text',
                    'required'    => 1,
                    'span'        => 'full',
                    'tab'         => 'Test',
                ],
                'viewBag[test_repeater]' => [
                    'prompt' => 'Add Data',
                    'type'   => 'repeater',
                    'groups' => [
                        'textarea' => [
                            'name'        => 'Textarea',
                            'description' => 'Basic text field',
                            'icon'        => 'icon-file-text-o',
                            'fields'      => [
                                'text_area' => [
                                    'label' => 'Text Content',
                                    'type'  => 'textarea',
                                    'size'  => 'large',
                                ],
                            ],
                        ],
                        'quote'    => [
                            'name'        => 'Quote',
                            'description' => 'Quote item',
                            'icon'        => 'icon-quote-right',
                            'fields'      => [
                                'quote_position' => [
                                    'span'    => 'auto',
                                    'label'   => 'Quote Position',
                                    'type'    => 'radio',
                                    'options' => [
                                        'left'   => 'Left',
                                        'center' => 'Center',
                                        'right'  => 'Right',
                                    ],
                                ],
                                'quote_content'  => [
                                    'span'  => 'auto',
                                    'label' => 'Details',
                                    'type'  => 'textarea',
                                ],
                            ],
                        ],
                        'image'    => [
                            'name'        => 'Image',
                            'description' => 'Pick something from the media library',
                            'icon'        => 'icon-photo',
                            'fields'      => [
                                'img_upload'   => [
                                    'span'        => 'auto',
                                    'label'       => 'Image',
                                    'type'        => 'mediafinder',
                                    'mode'        => 'image',
                                    'imageHeight' => 260,
                                    'imageWidth'  => 260,
                                ],
                                'img_position' => [
                                    'span'    => 'auto',
                                    'label'   => 'Image Position',
                                    'type'    => 'radio',
                                    'options' => [
                                        'left'   => 'Left',
                                        'center' => 'Center',
                                        'right'  => 'Right',
                                    ],
                                ],
                            ],
                        ],
                    ],
                    'span'   => 'full',
                    'tab'    => 'Test',
                ],
            ], 'primary');
            $model = $widget->model;
            $model->bindEvent('model.beforeSave', function () use ($model) {
                $model->rules += [
                    'viewBag[test_field]'    => 'required',
                    'viewBag[test_repeater]' => 'required',
                ];
            });
        });
    }
}

@seanthepottingshed If you use dot notation for the field names in the validation rules, it does work:

$model->bindEvent('model.beforeSave', function () use ($model) {
    $model->rules += [
        'viewBag.test_field'    => 'required',
        'viewBag.test_repeater' => 'required',
    ];
});

@bennothommo are there any changes we could make to the docs to make dot syntax more obvious?

@LukeTowers I'd say it's well covered in this section already. A potential enhancement that could be implemeted would be to do a pass on the fields in a rule array and converting array notation to dot notation when validating - this did trip me up when I first started using October.

@bennothommo so grateful can鈥檛 wait to try it!

@bennothommo I do like that idea, if you think it can be done easily go for it!

@LukeTowers On it :)

@LukeTowers @bennothommo is Octobercms.com still down for updates - can鈥檛 access the documentation link above

@seanthepottingshed Seems to be working fine for me.

@bennothommo yep I鈥檓 back in the room ;)

@LukeTowers PR here for array notation enhancement: https://github.com/octobercms/library/pull/383. I've tested with @seanthepottingshed 's PR to the Test plugin, and it works well.

@bennothommo @LukeTowers @w20k

Thanks guys I've just tested the dot syntax and all works as expected, for clarification should anyone else run into this prior to @bennothommo solution is merged into a future release:

public function boot()
{
    if (\App::runningInBackend()) {
        \Event::listen('backend.form.extendFields', function ($widget) {
            if (!$widget->model instanceof \Cms\Classes\Page) {
                return;
            }
            if ($widget->isNested || $widget->model->settings['url'] !== '/') {
                return;
            }
            $widget->addFields([
                'viewBag[test_field]'    => [
                    'label'       => 'Test Field',
                    'placeholder' => 'Enter text',
                    'required'    => 1,
                    'span'        => 'full',
                    'tab'         => 'Test',
                ],
                'viewBag[test_repeater]' => [
                    'prompt' => 'Add Data',
                    'type'   => 'repeater',
                    'groups' => [
                        'textarea' => [
                            'name'        => 'Textarea',
                            'description' => 'Basic text field',
                            'icon'        => 'icon-file-text-o',
                            'fields'      => [
                                'text_area' => [
                                    'label' => 'Text Content',
                                    'type'  => 'textarea',
                                    'size'  => 'large',
                                ],
                            ],
                        ],
                        'quote'    => [
                            'name'        => 'Quote',
                            'description' => 'Quote item',
                            'icon'        => 'icon-quote-right',
                            'fields'      => [
                                'quote_position' => [
                                    'span'    => 'auto',
                                    'label'   => 'Quote Position',
                                    'type'    => 'radio',
                                    'options' => [
                                        'left'   => 'Left',
                                        'center' => 'Center',
                                        'right'  => 'Right',
                                    ],
                                ],
                                'quote_content'  => [
                                    'span'  => 'auto',
                                    'label' => 'Details',
                                    'type'  => 'textarea',
                                ],
                            ],
                        ],
                        'image'    => [
                            'name'        => 'Image',
                            'description' => 'Pick something from the media library',
                            'icon'        => 'icon-photo',
                            'fields'      => [
                                'img_upload'   => [
                                    'span'        => 'auto',
                                    'label'       => 'Image',
                                    'type'        => 'mediafinder',
                                    'mode'        => 'image',
                                    'imageHeight' => 260,
                                    'imageWidth'  => 260,
                                ],
                                'img_position' => [
                                    'span'    => 'auto',
                                    'label'   => 'Image Position',
                                    'type'    => 'radio',
                                    'options' => [
                                        'left'   => 'Left',
                                        'center' => 'Center',
                                        'right'  => 'Right',
                                    ],
                                ],
                            ],
                        ],
                    ],
                    'span'   => 'full',
                    'tab'    => 'Test',
                ],
            ], 'primary');
            $model = $widget->model;
            $model->bindEvent('model.beforeValidate', function () use ($model) {
                $model->rules += [
                    'viewBag.test_field'    => 'required',
                    'viewBag.test_repeater' => 'required',
                ];
            });
        });
    }
}

@bennothommo @LukeTowers

I've run into another problem relating to this, when validating the actual repeater data itself, ie:

<?php
namespace October\Test;

use Backend;
use System\Classes\PluginBase;

/**
 * Test Plugin Information File
 */
class Plugin extends PluginBase
{
    /**
     * Returns information about this plugin.
     *
     * @return array
     */
    public function pluginDetails()
    {
        return [
            'name'        => 'October Tester',
            'description' => 'Used for testing the Relation Controller behavior and others.',
            'author'      => 'Alexey Bobkov, Samuel Georges',
            'icon'        => 'icon-child',
            'homepage'    => 'https://github.com/daftspunk/oc-test-plugin',
        ];
    }

    public function registerNavigation()
    {
        return [
            'test' => [
                'label'    => 'Playground',
                'url'      => Backend::url('october/test/people'),
                'icon'     => 'icon-child',
                'order'    => 200,

                'sideMenu' => [
                    'people'    => [
                        'label' => 'People',
                        'icon'  => 'icon-database',
                        'url'   => Backend::url('october/test/people'),
                    ],
                    'posts'     => [
                        'label' => 'Posts',
                        'icon'  => 'icon-database',
                        'url'   => Backend::url('october/test/posts'),
                    ],
                    'users'     => [
                        'label' => 'Users',
                        'icon'  => 'icon-database',
                        'url'   => Backend::url('october/test/users'),
                    ],
                    'countries' => [
                        'label' => 'Countries',
                        'icon'  => 'icon-database',
                        'url'   => Backend::url('october/test/countries'),
                    ],
                    'reviews'   => [
                        'label' => 'Reviews',
                        'icon'  => 'icon-database',
                        'url'   => Backend::url('october/test/reviews'),
                    ],
                    'galleries' => [
                        'label' => 'Galleries',
                        'icon'  => 'icon-database',
                        'url'   => Backend::url('october/test/galleries'),
                    ],
                    'trees'     => [
                        'label' => 'Trees',
                        'icon'  => 'icon-database',
                        'url'   => Backend::url('october/test/trees'),
                    ],
                    'pages'     => [
                        'label' => 'Pages',
                        'icon'  => 'icon-database',
                        'url'   => Backend::url('october/test/pages'),
                    ],
                ],
            ],
        ];
    }

    public function registerFormWidgets()
    {
        return [
            'October\Test\FormWidgets\TimeChecker' => [
                'code' => 'timecheckertest',
            ],
        ];
    }

    public function boot()
    {
        if (\App::runningInBackend()) {
            \Event::listen('backend.form.extendFields', function ($widget) {
                if (!$widget->model instanceof \Cms\Classes\Page) {
                    return;
                }
                if ($widget->isNested || $widget->model->settings['url'] !== '/') {
                    return;
                }
                $widget->addFields([
                    'viewBag[test_field]'    => [
                        'label'       => 'Test Field',
                        'placeholder' => 'Enter text',
                        'required'    => 1,
                        'span'        => 'full',
                        'tab'         => 'Test',
                    ],
                    'viewBag[test_repeater]' => [
                        'prompt' => 'Add Data',
                        'type'   => 'repeater',
                        'groups' => [
                            'textarea' => [
                                'name'        => 'Textarea',
                                'description' => 'Basic text field',
                                'icon'        => 'icon-file-text-o',
                                'fields'      => [
                                    'text_area' => [
                                        'label' => 'Text Content',
                                        'type'  => 'textarea',
                                        'size'  => 'large',
                                    ],
                                ],
                            ],
                            'quote'    => [
                                'name'        => 'Quote',
                                'description' => 'Quote item',
                                'icon'        => 'icon-quote-right',
                                'fields'      => [
                                    'quote_position' => [
                                        'span'    => 'auto',
                                        'label'   => 'Quote Position',
                                        'type'    => 'radio',
                                        'options' => [
                                            'left'   => 'Left',
                                            'center' => 'Center',
                                            'right'  => 'Right',
                                        ],
                                    ],
                                    'quote_content'  => [
                                        'span'  => 'auto',
                                        'label' => 'Details',
                                        'type'  => 'textarea',
                                    ],
                                ],
                            ],
                            'image'    => [
                                'name'        => 'Image',
                                'description' => 'Pick something from the media library',
                                'icon'        => 'icon-photo',
                                'fields'      => [
                                    'img_upload'   => [
                                        'span'        => 'auto',
                                        'label'       => 'Image',
                                        'type'        => 'mediafinder',
                                        'mode'        => 'image',
                                        'imageHeight' => 260,
                                        'imageWidth'  => 260,
                                    ],
                                    'img_position' => [
                                        'span'    => 'auto',
                                        'label'   => 'Image Position',
                                        'type'    => 'radio',
                                        'options' => [
                                            'left'   => 'Left',
                                            'center' => 'Center',
                                            'right'  => 'Right',
                                        ],
                                    ],
                                ],
                            ],
                        ],
                        'span'   => 'full',
                        'tab'    => 'Test',
                    ],
                ], 'primary');
                $model = $widget->model;
                $model->bindEvent('model.beforeValidate', function () use ($model) {
                    $model->rules += [
                        'viewBag.test_field'    => 'required',
                        'viewBag.test_repeater' => 'required',
                    ];
                    $model->customMessages = [
                        'viewBag.test_field.required'    => 'The Test Field is required',
                        'viewBag.test_repeater.required' => 'The Test Repeater field is required',
                    ];
                    if (!empty($model->test_repeater)) {
                        foreach ($model->test_repeater as $index => $repeater) {
                            $repeaterGroup = $repeater['_group'];
                            $repeaterRow   = 'viewBag.test_repeater.' . $index . '.';
                            switch ($repeaterGroup) {
                                case 'textarea':
                                    $model->rules[$repeaterRow . 'textarea.text_area']                   = 'required';
                                    //$model->customMessages[$repeaterRow . 'textarea.text_area.required'] = 'The Textarea group text area field is required';
                                    break;
                                case 'quote':
                                    $model->rules[$repeaterRow . 'quote.quote_position']                   = 'required';
                                    $model->rules[$repeaterRow . 'quote.quote_content']                    = 'required';
                                    //$model->customMessages[$repeaterRow . 'quote.quote_position.required'] = 'The Quote group quote position field is required';
                                    //$model->customMessages[$repeaterRow . 'quote.quote_content.required']  = 'The Quote group content position field is required';
                                    break;
                                case 'image':
                                    $model->rules[$repeaterRow . 'image.img_upload']                     = 'required';
                                    $model->rules[$repeaterRow . 'image.img_position']                   = 'required';
                                    //$model->customMessages[$repeaterRow . 'image.img_upload.required']   = 'The Image group quote position field is required';
                                    //$model->customMessages[$repeaterRow . 'image.img_position.required'] = 'The Image group content position field is required';
                                    break;
                            }
                        }
                    }
                });
            });
        }
    }
}

If you add a repeater group, it validates correctly the first time - however if you delete a repeater group and add another one, it still references the index of the previous entry.

Here we go again 馃槃

@seanthepottingshed @w20k I have a feeling that the Repeater widget's indexes are a big part of the recent problems. I think this particular issue is separate though to the issue I fixed recently, given that I was able to test the code above with an older build of October and it happened there too.

@bennothommo would you prefer it if I close this issue and raise a new issue with supporting fork of Test plugin to demonstrate?

@seanthepottingshed this issue report is fine. If I can't figure it out, I'll get you to create a new issue.

@bennothommo thanks your help in this matter is really appreciated

@bennothommo I've just tested again in 448 and this all seems to work now! I need to do some more testing just to make sure, but I think you cracked it.

@seanthepottingshed That's good to hear. I'm curious as to what has changed though, as my test copy is still exhibiting the issue. :-/

@bennothommo I will do some more testing over the weekend, it _was_ very late last night!

@bennothommo @w20k @LukeTowers @daftspunk

Yeah, you're right @bennothommo I was definitely having a senior moment, the issue remains.

@seanthepottingshed Thanks for letting me know - I will take a look at it in the next couple of days hopefully.

@seanthepottingshed I've had another look at this following on with the fixes in https://github.com/octobercms/october/pull/4242. The following boot() code does fix the validation:

public function boot()
    {
        if (\App::runningInBackend()) {
            \Event::listen('backend.form.extendFields', function ($widget) {
                if (!$widget->model instanceof \Cms\Classes\Page) {
                    return;
                }
                if ($widget->isNested || $widget->model->settings['url'] !== '/') {
                    return;
                }
                $widget->addFields([
                    'viewBag[test_field]'    => [
                        'label'       => 'Test Field',
                        'placeholder' => 'Enter text',
                        'required'    => 1,
                        'span'        => 'full',
                        'tab'         => 'Test',
                    ],
                    'viewBag[test_repeater]' => [
                        'prompt' => 'Add Data',
                        'type'   => 'repeater',
                        'groups' => [
                            'textarea' => [
                                'name'        => 'Textarea',
                                'description' => 'Basic text field',
                                'icon'        => 'icon-file-text-o',
                                'fields'      => [
                                    'text_area' => [
                                        'label' => 'Text Content',
                                        'type'  => 'textarea',
                                        'size'  => 'large',
                                    ],
                                ],
                            ],
                            'quote'    => [
                                'name'        => 'Quote',
                                'description' => 'Quote item',
                                'icon'        => 'icon-quote-right',
                                'fields'      => [
                                    'quote_position' => [
                                        'span'    => 'auto',
                                        'label'   => 'Quote Position',
                                        'type'    => 'radio',
                                        'options' => [
                                            'left'   => 'Left',
                                            'center' => 'Center',
                                            'right'  => 'Right',
                                        ],
                                    ],
                                    'quote_content'  => [
                                        'span'  => 'auto',
                                        'label' => 'Details',
                                        'type'  => 'textarea',
                                    ],
                                ],
                            ],
                            'image'    => [
                                'name'        => 'Image',
                                'description' => 'Pick something from the media library',
                                'icon'        => 'icon-photo',
                                'fields'      => [
                                    'img_upload'   => [
                                        'span'        => 'auto',
                                        'label'       => 'Image',
                                        'type'        => 'mediafinder',
                                        'mode'        => 'image',
                                        'imageHeight' => 260,
                                        'imageWidth'  => 260,
                                    ],
                                    'img_position' => [
                                        'span'    => 'auto',
                                        'label'   => 'Image Position',
                                        'type'    => 'radio',
                                        'options' => [
                                            'left'   => 'Left',
                                            'center' => 'Center',
                                            'right'  => 'Right',
                                        ],
                                    ],
                                ],
                            ],
                        ],
                        'span'   => 'full',
                        'tab'    => 'Test',
                    ],
                ], 'primary');
                $model = $widget->model;
                $model->bindEvent('model.beforeValidate', function () use ($model) {
                    $model->rules += [
                        'viewBag.test_field'    => 'required',
                        'viewBag.test_repeater' => 'required',
                    ];
                    $model->customMessages = [
                        'viewBag.test_field.required'    => 'The Test Field is required',
                        'viewBag.test_repeater.required' => 'The Test Repeater field is required',
                    ];
                    $viewBag = $model->getAttribute('viewBag');
                    $repeaterValues = (isset($viewBag['test_repeater'])) ? $viewBag['test_repeater'] : null;

                    if (!empty($repeaterValues)) {
                        foreach ($repeaterValues as $index => $item) {
                            $repeaterGroup = $item['_group'];
                            $repeaterRow   = 'viewBag.test_repeater.' . $index . '.';
                            switch ($repeaterGroup) {
                                case 'textarea':
                                    $model->rules[$repeaterRow . 'text_area']                   = 'required';
                                    break;
                                case 'quote':
                                    $model->rules[$repeaterRow . 'quote_position']                   = 'required';
                                    $model->rules[$repeaterRow . 'quote_content']                    = 'required';
                                    break;
                                case 'image':
                                    $model->rules[$repeaterRow . 'img_upload']                     = 'required';
                                    $model->rules[$repeaterRow . 'img_position']                   = 'required';
                                    break;
                            }
                        }
                    }
                });
            });
        }
    }

However, the saving of the repeater items is broken due to the indexes of the repeater item fields clashing. I'll probably have to look at this as part of my other fixes, but at the very least, this code should work once all the index fixes are sorted.

Should be definitely added to OctoberTricks 馃槃

@bennothommo

Thanks! This is so very appreciated. Should the CMS page repeater indices be fixed - then my life would be complete in every way imaginable. ;)

@seanthepottingshed Could you give my branch (https://github.com/octobercms/october/pull/4242) a test and let me know if this works for you? My latest commit should fix the index issue.

@seanthepottingshed your life should be complete now 馃槃 I hope)

@w20k @bennothommo

Sorry for massive delay in getting back to you guys, been on Easter holidays, will definitely get back onto this once I've caught up with client work. Thanks as always!

@seanthepottingshed Sounds good, and not a problem! You'll need to test it against the October develop branch now, as the repeater fixes are all merged.

@bennothommo

Sexytime!

Was this page helpful?
0 / 5 - 0 ratings