__('Form', __FILE__), // Module Title
'summary' => __('Contains one or more fields in a form', __FILE__), // Module Summary
'version' => 104,
'permanent' => true,
);
}
const debug = false; // set to true to enable debug mode for field dependencies
public function __construct() {
$this->set('protectCSRF', true);
parent::__construct();
$this->attr('method', 'post');
$this->attr('action', './');
$this->set('class', '');
}
protected function debugNote($note) {
if(self::debug) $this->message($note);
}
public function ___render() {
$this->attr('data-colspacing', (int) $this->columnWidthSpacing);
$this->attr('class', trim($this->attr('class') . ' InputfieldForm'));
$attrs = $this->getAttributes();
unset($attrs['value']);
if($this->input->get('modal') && strpos($attrs['action'], 'modal=1') === false) {
// retain a modal=1 state in the form action
$attrs['action'] .= (strpos($attrs['action'], '?') === false ? '?' : '&') . 'modal=1';
}
$description = $this->getSetting('description');
if($description) $description = "\n
" . $this->entityEncode($description) . "
";
$attrStr = $this->getAttributesString($attrs);
if($this->protectCSRF && $this->attr('method') == 'post') {
$tokenField = $this->wire('session')->CSRF->renderInput();
} else {
$tokenField = '';
}
$out = "\n";
return $out;
}
public function ___processInput(WireInputData $input) {
if($this->protectCSRF && $this->attr('method') == 'post') $this->wire('session')->CSRF->validate(); // throws exception if invalid
$result = parent::___processInput($input);
$delayedChildren = $this->_getDelayedChildren(true);
$delayedChildren = $this->processInputShowIf($input, $delayedChildren);
$this->processInputRequiredIf($input, $delayedChildren);
return $result;
}
/**
* Process input for show-if dependencies
*
* @param WireInputData $input
* @param array $delayedChildren
* @return array
*
*/
protected function processInputShowIf(WireInputData $input, array $delayedChildren) {
if(!count($delayedChildren)) return $delayedChildren;
$maxN = 255;
$n = 0;
$delayedN = count($delayedChildren);
$processedN = 0;
$unprocessedN = 0;
$savedChildren = $delayedChildren;
while(count($delayedChildren)) {
if(++$n >= $maxN) {
$this->error("Max number of iterations reached for processing field dependencies", Notice::debug);
break;
}
// shift first $child off the array
$child = array_shift($delayedChildren);
$this->debugNote("Processing delayed child: $child->name ($child->label)");
$selectorString = $child->getSetting('showIf');
if(!strlen($selectorString)) {
$this->debugNote("Skipping $child->name ($child->label): No selector string");
continue;
}
$this->debugNote("showIf selector: $selectorString");
$selectors = new Selectors($selectorString);
// whether we should process $child now or not
$processNow = true;
foreach($selectors as $selector) {
$fields = is_array($selector->field) ? $selector->field : array($selector->field);
// first determine that the dependency fields have already been processed
foreach($fields as $name) {
$this->debugNote("$child->name requires: $name");
if(isset($savedChildren[$name]) && $name !== "1") {
// if field had already been through the loop, but was not processed, add it back in for processing
if(!isset($delayedChildren[$name]) && !$savedChildren[$name]->showIfProcessed) $delayedChildren[$name] = $savedChildren[$name];
// force $delayedChildren[$name] to match so that it is processed here, by giving it special selctor: 1>0
if(!strlen($savedChildren[$name]->getSetting('showIf'))) $savedChildren[$name]->showIf = '1>0'; // forced match
// dependency $field is another one in $delayedChildren, send it back to the end
if(!$savedChildren[$name]->showIfProcessed) {
unset($delayedChildren[$child->name]);
// put it back on the end
$delayedChildren[$child->name] = $child;
$this->debugNote("Sending field '$child->name' back to the end.");
$processNow = false;
}
break;
} else {
// $field is most likely a form field has already been processed and is good to use
$processNow = true;
}
}
if(!$processNow) break; // out to next $child
// good to process $child
foreach($fields as $name) {
if($name == '1') continue;
$subfield = '';
if(strpos($name, '.')) list($name, $subfield) = explode('.', $name);
// get the inputfield that $child has a dependency on
$dependencyChild = $this->getChildByName($name);
// if field is not present in this form, we assume a blank value for it
if(!$dependencyChild) {
$this->error("Warning: dependency field '$name' is not present in this form.", Notice::debug);
continue;
}
$value = $dependencyChild->attr('value');
if($subfield == 'count') $value = count($value);
if(is_object($value)) $value = "$value";
if(!$selector->matches($value)) {
$this->debugNote("Selector failed to match \"$selector\" because value=" . print_r($value, true));
$processNow = false;
break;
}
} // $fields
if(!$processNow) break;
$this->debugNote("$child->name ($child->label) - matched: showIf($selector)");
$processedN++;
} // $selectors
if($subfield == 'count') $this->debugNote("actual count: $value");
if(!$processNow) {
$this->debugNote("$child->name ($child->label) - did not match: showIf($selector)");
$this->debugNote("Skipped processing for: $child->name ($child->label)");
$child->set('showIfSkipped', true); // flag the field as skipped
$unprocessedN++;
// since this didn't match, then no other selectors in the group for this child can match, so break out of the selector loop
continue; // to next $child
}
// the required dependency is in place so that $child can be processed
// temporarily remove the showIf property to prevent InputfieldWrapper's from delaying it again
$showIf = $child->getSetting('showIf');
$child->set('showIf', ''); // remove showIf property
$child->processInput($input); // process input
if($showIf != '1>0') $child->set('showIf', $showIf); // restore showIf property
$child->set('showIfProcessed', true); // flag it as processed
$this->debugNote("$child->name - processed!");
// now check if the processed child has children of it's own that may have been delayed
if($child instanceof InputfieldWrapper) {
$delayed = $child->_getDelayedChildren(true);
if(count($delayed)) {
foreach($delayed as $d) {
$dname = $d->attr('name');
if(!$dname) $dname = $d->attr('id');
$this->debugNote("Delayed: $dname ($d->label)");
}
$delayedChildren = array_merge($delayedChildren, $delayed); // add them to delayed children
$savedChildren = array_merge($savedChildren, $delayed); // add them to saved children (to be sent to requiredIf too)
$delayedN += count($delayed);
}
}
} // count($delayedChildren)
$this->debugNote("delayedChildren: $delayedN ($processedN processed, $unprocessedN not)");
return $savedChildren;
}
/**
* Process input for fields with a required-if dependency
*
* @param WireInputData $input
* @param array $delayedChildren
* @return bool
*
*/
protected function processInputRequiredIf(WireInputData $input, array $delayedChildren) {
// process input for any remaining delayedChildren not already processed by processInputShowIf
foreach($delayedChildren as $child) {
if($child->showIfSkipped || $child->showIfProcessed) continue;
$this->debugNote("Now Processing delayed child: $child->name");
$child->processInput($input);
}
while(count($delayedChildren)) {
// shift first $child off the array
$child = array_shift($delayedChildren);
if(!$child->getSetting('required')) continue;
// if field was not shown, then it can't be required
if($child->showIfSkipped) continue;
$required = true;
$selectorString = $child->getSetting('requiredIf');
if(strlen($selectorString)) {
$this->debugNote("requiredIf selector: $selectorString");
$selectors = new Selectors($selectorString);
foreach($selectors as $selector) {
$fields = is_array($selector->field) ? $selector->field : array($selector->field);
foreach($fields as $name) {
$subfield = '';
if(strpos($name, '.')) list($name, $subfield) = explode('.', $name);
// get the inputfield that $child has a dependency on
$dependencyChild = $this->getChildByName($name);
// if field is not present in this form, we assume a blank value for it
if(!$dependencyChild) {
$this->error("Warning: required dependency field '$name' is not present in this form.", Notice::debug);
continue;
}
$value = $dependencyChild->attr('value');
if($subfield == 'count') $value = count($value);
if(is_object($value)) $value = "$value";
if(!$selector->matches($value)) {
$this->debugNote("$name ($name.$subfield) - did not match: requiredIf($selector)");
$required = false;
break;
}
} // foreach($fields)
if(!$required) break;
} // foreach($selectors)
} // if(strlen($selectorString))
if($required) {
if($child->isEmpty()) {
$this->debugNote("$child->name - determined that value IS required and is not present (error)");
$child->error($this->requiredLabel); // requiredLabel from InputfieldWrapper
} else {
$this->debugNote("$child->name - determined that value IS required and is populated (good)");
}
} else {
$this->debugNote("$child->name - determined that value is not required");
}
}
}
}