silverstripe / silverstripe-userforms

UserForms module provides a visual form builder for the Silverstripe CMS. No coding required to build forms such as contact pages.
BSD 3-Clause "New" or "Revised" License
135 stars 225 forks source link

Extend PHP Validation to validate required fields by custom rules #131

Open wilr opened 11 years ago

wilr commented 11 years ago

From https://github.com/silverstripe/silverstripe-userforms/issues/116

Create textfield test. Create new textfield test1. Mark it required & set it to hide by default. Add custom rule to test1: Show This Field When test is Value test

Try submitting the form with field test set to check. The form won't validate. Now set it to test. Field test1 appears correctly. And if it is unset, the form won't pass validation. Setting test1 validates the form.

tazzydemon commented 10 years ago

Any progress on this issue which has reared its ugly head with me. I'm almost prepared to put up with no php validation at this point,.

wilr commented 10 years ago

No progress on it from me. Happy to review patches if you want to give it a go. If you want to disable PHP validation you may as well make the field not required and just put (required) as a label text or something.

jyrkij commented 9 years ago

If I'll try patching this at some point where should I start my searches for PHP side of validation. As I mentioned in #116 I couldn't find the place then...

wilr commented 9 years ago

The PHP side of validation uses the normal SilverStripe API. See UserDefinedForm::getRequiredFields(). One way would be to not use the core SilverStripe validate() method on form fields and manually invoke validate methods in the submit form method.

tazzydemon commented 9 years ago

Since I just failed so spectacularly on identifying a jquery validation error, ironically because of this, I was wondering if anybody had any more to offer. I cant see a way round it other than to not require the conditional fields and the client is demanding that they are required. A problem and perhaps time for another custom form!

wilr commented 9 years ago

@tazzydemon What I would aim to do is ticking that required box in the cms didn't actually set the PHP required fields and validation is controlled after the rules are parsed in the form handler. I'm keen to build this when I can get around to funding it, if you want to tackle it PR's welcome.

doniz commented 8 years ago

Hi guys, i a little bit working on this. I created a method which is remove field from required fields if it's not pass the rules of dependencies. But this method need to be improved with radio, checkbox and checkbox groups.

here is the code userforms/code/model/UserDefinedForm.php of UserDefinedForm_Controller:


    /**
     * Method will checks all required fields within post data, and check did it
     * need to remove field from required fields if field rules (dependencies) are negative.
     *
     * @param RequiredFields $requiredFields Set required fields
     * @param array          $post $_POST data
     *
     * @return bool | returns false when no post array are given and true when scripts end.
     */
    public function requiredFieldsByRules(RequiredFields &$requiredFields, array $post = []) {
        if(count($post) <= 0) return false;

        foreach($requiredFields->getRequired() as $name) {
            if(!isset($post[$name]) || empty($post[$name])) {
                // field is required, but is not set or empty
                // check the rules expression, maybe this field is hidden
                if(($fields = $this->Fields()->filter('Name', $name)) && $fields->exists()) {
                    /** @var EditableFormField $field */
                    $field = $fields->first();
                    $remove = false;

                    foreach($field->CustomRules() as $rule) {
                        $rule = $rule->toMap();
                        $remove = (string) $rule['Display'] === 'Show' ? false : true;
                        $expectedValue = $rule['Value'];
                        $conditionFieldName = $rule['ConditionField'];
                        $postConditionFieldValue = isset($post[$conditionFieldName]) ? $post[$conditionFieldName] : '';
                        $conditionField = !empty($conditionFieldName) ? $this->Fields()->filter('Name', $conditionFieldName) : null;

                        if(!is_null($conditionField) && $conditionField->exists()) {
                            /** @var EditableFormField $conditionField */
                            $conditionField = $conditionField->first();
                        }

                        // todo: improve with radio, checkbox, and checkbox group inputs
                        switch($rule['ConditionOption']) {
                            case 'IsNotBlank':

                                if(empty($postConditionFieldValue)) {
                                    $remove = !$remove;
                                }

                                break;
                            case 'IsBlank':

                                if(!empty($postConditionFieldValue)) {
                                    $remove = !$remove;
                                }

                                break;

                            case 'HasValue':

                                if($postConditionFieldValue != $expectedValue) {
                                    $remove = !$remove;
                                }

                                break;

                            case 'ValueLessThan':

                                if((float) $postConditionFieldValue >= (float) $expectedValue) {
                                    $remove = !$remove;
                                }

                                break;

                            case 'ValueLessThanEqual':

                                if((float) $postConditionFieldValue > (float) $expectedValue) {
                                    $remove = !$remove;
                                }

                                break;

                            case 'ValueGreaterThan':

                                if((float) $postConditionFieldValue <= (float) $expectedValue) {
                                    $remove = !$remove;
                                }

                                break;

                            case 'ValueGreaterThanEqual':

                                if((float) $postConditionFieldValue < (float) $expectedValue) {
                                    $remove = !$remove;
                                }

                                break;

                            default: // ==HasNotValue

                                if($postConditionFieldValue == $expectedValue) {
                                    $remove = !$remove;
                                }

                                break;
                        }
                    }

                    if($remove) {
                        // remove field from required fields
                        $requiredFields->removeRequiredField($name);
                    }
                }
            }
        }

        return true;
    }

and in the Form method need to add this line:


// remove required fields if it's not pass the dependencies
$this->requiredFieldsByRules($required, Convert::raw2xml($_POST));

example:


    /**
     * Get the form for the page. Form can be modified by calling {@link updateForm()}
     * on a UserDefinedForm extension.
     *
     * @return Form|false
     */
    public function Form() {
        $fields = $this->getFormFields();
        if(!$fields || !$fields->exists()) return false;

        $actions = $this->getFormActions();

        // get the required fields including the validation
        $required = $this->getRequiredFields();

        // remove required fields if it's not pass the dependencies
        $this->requiredFieldsByRules($required, Convert::raw2xml($_POST));

        // generate the conditional logic 
        $this->generateConditionalJavascript();

        $form = new Form($this, "Form", $fields, $actions, $required);
        $form->setRedirectToFormOnValidationError(true);

        $data = Session::get("FormInfo.{$form->FormName()}.data");

        if(is_array($data)) $form->loadDataFrom($data);

        $this->extend('updateForm', $form);

        if($this->DisableCsrfSecurityToken) {
            $form->disableSecurityToken();
        }

        return $form;
    }

So when field will have a rules, this method will check is it set as required field and try to check if rules are negative.

dhensby commented 8 years ago

@Doniz thanks for taking the time to do this - it may be best to move this discussion to a pull request - would you mind opening one with your code changes?

tractorcow commented 8 years ago

Another challenge to overcome is the configuration of jquery-validate on the frontend; It will require to be updated whenever field visibility is modified in order to not require hidden fields.

Also please note the PHP server side validation logic which fails when you mark a field as both required and having custom rules. :)

It's not going to be an easy challenge to solve in any case!

I'm going to downgrade this from a bug, as the bug of "required field is conditionally hidden" was actually prevented by the aforementioned validation rule. See https://github.com/silverstripe/silverstripe-userforms/blob/a3d425d443714157c23c3e393b8d0d498f12a3b2/code/model/editableformfields/EditableFormField.php#L238

nzphoenix commented 5 years ago

Has anyone had any more thoughts on this since SS4 was released? Had a couple of clients keen to do this, but understand the current limitation.. Wondering if SS4 might make it any more possible?