diff --git a/code/Form/UserFormsRequiredFields.php b/code/Form/UserFormsRequiredFields.php index 02aac42..bce0beb 100644 --- a/code/Form/UserFormsRequiredFields.php +++ b/code/Form/UserFormsRequiredFields.php @@ -2,6 +2,7 @@ namespace SilverStripe\UserForms\Form; +use InvalidArgumentException; use SilverStripe\Dev\Debug; use SilverStripe\Forms\FileField; use SilverStripe\Forms\FormField; @@ -27,7 +28,7 @@ class UserFormsRequiredFields extends RequiredFields * * @param array $data * - * @return boolean + * @return bool */ public function php($data) { @@ -58,18 +59,14 @@ class UserFormsRequiredFields extends RequiredFields // get editable form field - owns display rules for field $editableFormField = $this->getEditableFormFieldByName($fieldName); - $error = false; - - // validate if there are no display rules or the field is conditionally visible - if (!$this->hasDisplayRules($editableFormField) || - $this->conditionalFieldEnabled($editableFormField, $data)) { - $error = $this->validateRequired($formField, $data); - } + // Validate if the field is displayed + $error = + $editableFormField->isDisplayed($data) && + $this->validateRequired($formField, $data); // handle error case if ($formField && $error) { $this->handleError($formField, $fieldName); - $valid = false; } } @@ -77,57 +74,35 @@ class UserFormsRequiredFields extends RequiredFields return $valid; } + /** + * Retrieve an Editable Form field by its name. + * @param string $name + * @return EditableFormField + */ private function getEditableFormFieldByName($name) { - return EditableFormField::get()->filter(['name' => $name])->first(); - } + $field = EditableFormField::get()->filter(['Name' => $name])->first(); - private function hasDisplayRules($field) - { - return ($field->DisplayRules()->count() > 0); - } - - private function conditionalFieldEnabled($editableFormField, $data) - { - $displayRules = $editableFormField->DisplayRules(); - - $conjunction = $editableFormField->DisplayRulesConjunctionNice(); - - $displayed = ($editableFormField->ShowOnLoadNice() === 'show'); - - // && start with true and find and condition that doesn't satisfy - // || start with false and find and condition that satisfies - $conditionsSatisfied = ($conjunction === '&&'); - - foreach ($displayRules as $rule) { - $controllingField = EditableFormField::get()->byID($rule->ConditionFieldID); - - if ($controllingField->DisplayRules()->count() > 0) { // controllingField is also a conditional field - // recursively check - if any of the dependant fields are hidden, then this field cannot be visible. - if ($this->conditionalFieldEnabled($controllingField, $data)) { - return false; - }; - } - - $ruleSatisfied = $rule->validateAgainstFormData($data); - - if ($conjunction === '||' && $ruleSatisfied) { - $conditionsSatisfied = true; - break; - } - if ($conjunction === '&&' && !$ruleSatisfied) { - $conditionsSatisfied = false; - break; - } + if ($field) { + return $field; } - // initially displayed - condition fails || initially hidden, condition passes - return ($displayed xor $conditionsSatisfied); + // This should happen if form field data got corrupted + throw new InvalidArgumentException(sprintf( + 'Could not find EditableFormField with name `%s`', + $name + )); } - // logic replicated from php() method of parent class SilverStripe\Forms\RequiredFields - // TODO refactor to share with parent (would require corrosponding change in framework) - private function validateRequired($field, $data) + /** + * Check if the validation rules for the specified field are met by the provided data. + * + * @note Logic replicated from php() method of parent class `SilverStripe\Forms\RequiredFields` + * @param EditableFormField $field + * @param array $data + * @return bool + */ + private function validateRequired(FormField $field, array $data) { $error = false; $fieldName = $field->getName(); diff --git a/code/Model/EditableFormField.php b/code/Model/EditableFormField.php index 1f02ed7..2ec4f8f 100755 --- a/code/Model/EditableFormField.php +++ b/code/Model/EditableFormField.php @@ -57,7 +57,7 @@ use Symbiote\GridFieldExtensions\GridFieldEditableColumns; * @property boolean $ShowOnLoad * @property string $DisplayRulesConjunction * @method UserDefinedForm Parent() Parent page - * @method DataList DisplayRules() List of EditableCustomRule objects + * @method DataList|EditableCustomRule[] DisplayRules() List of EditableCustomRule objects * @mixin Versioned */ class EditableFormField extends DataObject @@ -977,6 +977,48 @@ class EditableFormField extends DataObject return (count($result['selectors'])) ? $result : null; } + /** + * Check if this EditableFormField is displayed based on its DisplayRules and the provided data. + * @param array $data + * @return bool + */ + public function isDisplayed(array $data) + { + $displayRules = $this->DisplayRules(); + + if ($displayRules->count() === 0) { + // If no display rule have been defined, isDisplayed equals the ShowOnLoad property + return $this->ShowOnLoad; + } + + $conjunction = $this->DisplayRulesConjunctionNice(); + + // && start with true and find and condition that doesn't satisfy + // || start with false and find and condition that satisfies + $conditionsSatisfied = ($conjunction === '&&'); + + foreach ($displayRules as $rule) { + $controllingField = $rule->ConditionField(); + + // recursively check - if any of the dependant fields are hidden, assume the rule can not be satisfied + $ruleSatisfied = $controllingField->isDisplayed($data) && $rule->validateAgainstFormData($data); + + if ($conjunction === '||' && $ruleSatisfied) { + $conditionsSatisfied = true; + break; + } + if ($conjunction === '&&' && !$ruleSatisfied) { + $conditionsSatisfied = false; + break; + } + } + + // initially displayed - condition fails || initially hidden, condition passes + $startDisplayed = $this->ShowOnLoad; + return ($startDisplayed xor $conditionsSatisfied); + } + + /** * Replaces the set DisplayRulesConjunction with their JS logical operators * @return string diff --git a/tests/Model/EditableFormFieldTest.php b/tests/Model/EditableFormFieldTest.php index c1580de..80de25b 100644 --- a/tests/Model/EditableFormFieldTest.php +++ b/tests/Model/EditableFormFieldTest.php @@ -232,4 +232,58 @@ class EditableFormFieldTest extends FunctionalTest $this->assertContains('/images/editabletextfield.png', $field->getIcon()); } + + public function displayedProvider() + { + $one = ['basic_text_name' => 'foobar']; + $two = array_merge($one, ['basic_text_name_2' => 'foobar']); + + return [ + 'no display rule AND' => ['alwaysVisible', [], true], + 'no display rule OR' => ['alwaysVisibleOr', [], true], + + 'no display rule hidden AND' => ['neverVisible', [], false], + 'no display rule hidden OR' => ['neverVisibleOr', [], false], + + '1 unmet display rule AND' => ['singleDisplayRule', [], false], + '1 met display rule AND' => ['singleDisplayRule', $one, true], + '1 unmet display rule OR' => ['singleDisplayRuleOr', [], false], + '1 met display rule OR' => ['singleDisplayRuleOr', $one, true], + + '1 unmet hide rule AND' => ['singleHiddingRule', [], true], + '1 met hide rule AND' => ['singleHiddingRule', $one, false], + '1 unmet hide rule OR' => ['singleHiddingRuleOr', [], true], + '1 met hide rule OR' => ['singleHiddingRuleOr', $one, false], + + 'multi display rule AND none met' => ['multiDisplayRule', [], false], + 'multi display rule AND partially met' => ['multiDisplayRule', $one, false], + 'multi display rule AND all met' => ['multiDisplayRule', $two, true], + + 'multi display rule OR none met' => ['multiDisplayRuleOr', [], false], + 'multi display rule OR partially met' => ['multiDisplayRuleOr', $one, true], + 'multi display rule OR all met' => ['multiDisplayRuleOr', $two, true], + + 'multi hide rule AND none met' => ['multiHiddingRule', [], true], + 'multi hide rule AND partially met' => ['multiHiddingRule', $one, true], + 'multi hide rule AND all met' => ['multiHiddingRule', $two, false], + + 'multi hide rule OR none met' => ['multiHiddingRuleOr', [], true], + 'multi hide rule OR partially met' => ['multiHiddingRuleOr', $one, false], + 'multi hide rule OR all met' => ['multiHiddingRuleOr', $two, false], + ]; + } + + /** + * @param $fieldName + * @param $data + * @param $expected + * @dataProvider displayedProvider + */ + public function testIsDisplayed($fieldName, $data, bool $expected) + { + /** @var EditableFormField $field */ + $field = $this->objFromFixture(EditableTextField::class, $fieldName); + $this->assertEquals($expected, $field->isDisplayed($data)); + } + } diff --git a/tests/Model/EditableFormFieldTest.yml b/tests/Model/EditableFormFieldTest.yml index b514964..d5f8695 100644 --- a/tests/Model/EditableFormFieldTest.yml +++ b/tests/Model/EditableFormFieldTest.yml @@ -4,7 +4,7 @@ SilverStripe\UserForms\Model\EditableFormField\EditableTextField: Title: Basic Text Field basic-text-2: - Name: basic_text_name + Name: basic_text_name_2 Title: Basic Text Field required-text: @@ -23,6 +23,83 @@ SilverStripe\UserForms\Model\EditableFormField\EditableTextField: DisplayRulesConjunction: And ShowOnLoad: false + # No rule + alwaysVisible: + Name: AlwaysVisible + Title: "This field is always visible" + ShowOnLoad: true + DisplayRulesConjunction: And + + alwaysVisibleOr: + Name: AlwaysVisibleOr + Title: "This field is always visible" + ShowOnLoad: true + DisplayRulesConjunction: Or + + neverVisible: + Name: NeverVisible + Title: "This field is never visible" + ShowOnLoad: false + DisplayRulesConjunction: And + + neverVisibleOr: + Name: NeverVisibleOr + Title: "This field is never visible" + ShowOnLoad: false + DisplayRulesConjunction: Or + + # Single rule + + singleDisplayRule: + Name: SingleDisplayRule + Title: "This field will be displayed if the display rule is tripped" + ShowOnLoad: false + DisplayRulesConjunction: And + + singleDisplayRuleOr: + Name: SingleDisplayRuleOr + Title: "This field will be displayed if the display rule is tripped" + ShowOnLoad: false + DisplayRulesConjunction: Or + + singleHiddingRule: + Name: SingleHiddingRule + Title: "This field will be hidden if the display rule is tripped" + ShowOnLoad: true + DisplayRulesConjunction: And + + singleHiddingRuleOr: + Name: SingleHiddingRuleOr + Title: "This field will be hidden if the display rule is tripped" + ShowOnLoad: true + DisplayRulesConjunction: Or + + # Multi rule + multiDisplayRule: + Name: MultiDisplayRule + Title: "This field will be displayed if displayed if all the rule are met" + ShowOnLoad: false + DisplayRulesConjunction: And + + multiDisplayRuleOr: + Name: MultiDisplayRuleOr + Title: "This field will be displayed if at least one rule is met" + ShowOnLoad: false + DisplayRulesConjunction: Or + + multiHiddingRule: + Name: MultiHiddingRule + Title: "This field will be hidden if all the rule are met" + ShowOnLoad: true + DisplayRulesConjunction: And + + multiHiddingRuleOr: + Name: MultiHiddingRuleOr + Title: "This field will be hidden if one rule is met" + ShowOnLoad: true + DisplayRulesConjunction: Or + + SilverStripe\UserForms\Model\EditableCustomRule: rule1: Display: Show @@ -35,6 +112,66 @@ SilverStripe\UserForms\Model\EditableCustomRule: ConditionOption: HasValue FieldValue: 6 + # Single rules + ruleSingleDisplay: + Display: Show + ConditionOption: IsNotBlank + ConditionField: =>SilverStripe\UserForms\Model\EditableFormField\EditableTextField.basic-text + Parent: =>SilverStripe\UserForms\Model\EditableFormField\EditableTextField.singleDisplayRule + ruleSingleDisplayOr: + Display: Show + ConditionOption: IsNotBlank + ConditionField: =>SilverStripe\UserForms\Model\EditableFormField\EditableTextField.basic-text + Parent: =>SilverStripe\UserForms\Model\EditableFormField\EditableTextField.singleDisplayRuleOr + ruleSingleHidding: + Display: Show + ConditionOption: IsNotBlank + ConditionField: =>SilverStripe\UserForms\Model\EditableFormField\EditableTextField.basic-text + Parent: =>SilverStripe\UserForms\Model\EditableFormField\EditableTextField.singleHiddingRule + ruleSingleHiddingOr: + Display: Show + ConditionOption: IsNotBlank + ConditionField: =>SilverStripe\UserForms\Model\EditableFormField\EditableTextField.basic-text + Parent: =>SilverStripe\UserForms\Model\EditableFormField\EditableTextField.singleHiddingRuleOr + + # Multi rules + ruleMultiDisplay1: + ConditionOption: IsNotBlank + ConditionField: =>SilverStripe\UserForms\Model\EditableFormField\EditableTextField.basic-text + Parent: =>SilverStripe\UserForms\Model\EditableFormField\EditableTextField.multiDisplayRule + ruleMultiDisplay2: + ConditionOption: IsNotBlank + ConditionField: =>SilverStripe\UserForms\Model\EditableFormField\EditableTextField.basic-text-2 + Parent: =>SilverStripe\UserForms\Model\EditableFormField\EditableTextField.multiDisplayRule + + ruleMultiDisplayOr1: + ConditionOption: IsNotBlank + ConditionField: =>SilverStripe\UserForms\Model\EditableFormField\EditableTextField.basic-text + Parent: =>SilverStripe\UserForms\Model\EditableFormField\EditableTextField.multiDisplayRuleOr + ruleMultiDisplayOr2: + ConditionOption: IsNotBlank + ConditionField: =>SilverStripe\UserForms\Model\EditableFormField\EditableTextField.basic-text-2 + Parent: =>SilverStripe\UserForms\Model\EditableFormField\EditableTextField.multiDisplayRuleOr + + + ruleMultiHidding1: + ConditionOption: IsNotBlank + ConditionField: =>SilverStripe\UserForms\Model\EditableFormField\EditableTextField.basic-text + Parent: =>SilverStripe\UserForms\Model\EditableFormField\EditableTextField.multiHiddingRule + ruleMultiHidding2: + ConditionOption: IsNotBlank + ConditionField: =>SilverStripe\UserForms\Model\EditableFormField\EditableTextField.basic-text-2 + Parent: =>SilverStripe\UserForms\Model\EditableFormField\EditableTextField.multiHiddingRule + + ruleMultiHiddingOr1: + ConditionOption: IsNotBlank + ConditionField: =>SilverStripe\UserForms\Model\EditableFormField\EditableTextField.basic-text + Parent: =>SilverStripe\UserForms\Model\EditableFormField\EditableTextField.multiHiddingRuleOr + ruleMultiHiddingOr2: + ConditionOption: IsNotBlank + ConditionField: =>SilverStripe\UserForms\Model\EditableFormField\EditableTextField.basic-text-2 + Parent: =>SilverStripe\UserForms\Model\EditableFormField\EditableTextField.multiHiddingRuleOr + SilverStripe\UserForms\Model\EditableFormField\EditableOption: option-1: Name: Option1