input format (column widths do not apply) * InputfieldFormConfirm: tell it to notify user if they make any changes and forgot to submit. * * #pw-body = * Here is an example of creating an InputfieldForm using Inputfield modules. This particular example * is an email subscription form. * ~~~~~ * $form = $modules->get('InputfieldForm'); * * $field = $modules->get('InputfieldText'); * $field->attr('name', 'your_name'); * $field->label = 'Your Name'; * $form->add($field); * * $field = $modules->get('InputfieldEmail'); * $field->attr('name', 'your_email'); * $field->label = 'Your Email Address'; * $field->required = true; * $form->add($field); * * $submit = $modules->get('InputfieldSubmit'); * $submit->attr('name', 'submit_subscribe'); * $form->add($submit); * * if($input->post('submit_subscribe')) { * // form submitted * $form->processInput($input->post); * $errors = $form->getErrors(); * if(count($errors)) { * // unsuccessful submit, re-display form * echo "

There were errors, please fix

"; * echo $form->render(); * } else { * // successful submit (save $name and $email somewhere) * $name = $form->get('your_name')->attr('value'); * $email = $form->get('your_email')->attr('value'); * echo "

Thank you, you have been subscribed!

"; * } * } else { * // form not submitted, just display it * echo $form->render(); * } * ~~~~~ * * #pw-body * */ class InputfieldForm extends InputfieldWrapper { public static function getModuleInfo() { return array( 'title' => __('Form', __FILE__), // Module Title 'summary' => __('Contains one or more fields in a form', __FILE__), // Module Summary 'version' => 107, 'permanent' => true, ); } const debug = false; // set to true to enable debug mode for field dependencies /** * WireInputData provided to processInput() method * * @var WireInputData|null * */ protected $_processInputData = null; /** * Construct * */ public function __construct() { $this->set('protectCSRF', true); parent::__construct(); $this->attr('method', 'post'); $this->attr('action', './'); $this->set('class', ''); $this->set('prependMarkup', ''); $this->set('appendMarkup', ''); $this->set('confirmText', $this->_('There are unsaved changes:')); } /** * Render form * * @return string * */ public function ___render() { if($this->hasHook('renderOrProcessReady()')) $this->renderOrProcessReady('render'); $markup = self::getMarkup(); $classes = self::getClasses(); if(!empty($classes['form'])) $this->addClass($classes['form']); $this->attr('data-colspacing', (int) $this->getSetting('columnWidthSpacing')); $this->addClass('InputfieldForm'); if($this->hasClass('InputfieldFormConfirm')) { if($this->wire('modules')->isInstalled('FormSaveReminder')) { // let FormSaveReminder module have control, if it's installed $this->removeClass('InputfieldFormConfirm'); } else { $this->attr('data-confirm', $this->getSetting('confirmText')); } } $attrs = $this->getAttributes(); unset($attrs['value']); if($this->wire('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 = str_replace('{out}', $this->entityEncode($description), $markup['item_head']); $attrStr = $this->getAttributesString($attrs); if($this->getSetting('protectCSRF') && strtolower($this->attr('method')) == 'post') { $tokenField = $this->wire('session')->CSRF->renderInput(); } else { $tokenField = ''; } /* @todo 3.0.125 $name = $this->getAttribute('name'); if(!empty($name)) { $name = $this->wire('sanitizer')->entities($name); $class = $this->className(); $tokenField .= ""; } */ $out = "
" . $description . $this->getSetting('prependMarkup') . parent::___render() . $tokenField . $this->getSetting('appendMarkup') . "
"; return $out; } /** * Process input * * @param WireInputData $input * @return InputfieldWrapper * */ public function ___processInput(WireInputData $input) { $this->_processInputData = $input; if($this->hasHook('renderOrProcessReady()')) $this->renderOrProcessReady('process'); $this->getErrors(true); // reset if($this->getSetting('protectCSRF') && strtolower($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; /** @var Inputfield[] $savedChildren */ $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); if(self::debug) $this->debugNote("Processing delayed child: $child->name ($child->label)"); $selectorString = $child->getSetting('showIf'); if(!strlen($selectorString)) { if(self::debug) $this->debugNote("Skipping $child->name ($child->label): No selector string"); continue; } if(self::debug) $this->debugNote("showIf selector: $selectorString"); $selectors = $this->wire(new Selectors($selectorString)); // whether we should process $child now or not $processNow = true; $selector = null; 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) { if(self::debug) $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]->getSetting('showIfProcessed') && !$savedChildren[$name]->getSetting('showIfSkipped')) { $delayedChildren[$name] = $savedChildren[$name]; } // force $delayedChildren[$name] to match so that it is processed here, by giving it special selector: 1>0 if(!strlen($savedChildren[$name]->getSetting('showIf'))) { $savedChildren[$name]->showIf = '1>0'; // forced match } if($savedChildren[$name]->getSetting('showIfSkipped')) { // dependency $field does not need to be processed, so neither does this field list($val, $op) = array($selector->value, $selector->operator); if(($op === '!=' && $val) || $op === '=' && empty($val)) { // allow it through to match non-presence (via issue #890) } else { unset($delayedChildren[$child->name]); $processNow = false; if(self::debug) $this->debugNote("Removing field '$child->name' because '$name' it not shown."); } } else if(!$savedChildren[$name]->getSetting('showIfProcessed')) { // dependency $field is another one in $delayedChildren, send it back to the end unset($delayedChildren[$child->name]); // put it back on the end $delayedChildren[$child->name] = $child; if(self::debug) $this->debugNote("Sending field '$child->name' back to the end."); $processNow = false; } break; } else { // $field is most likely a form field that has already been processed and is good to use $processNow = true; } } // foreach($fields) if(!$processNow) break; // out to next $child $numFieldsMatched = 0; // good to process $child foreach($fields as $name) { if($name == '1') { $numFieldsMatched++; } else if($this->selectorMatchesInputfield($selector, $name, "'showIf' from '$child->label'")) { $numFieldsMatched++; } } // $fields $processNow = $numFieldsMatched > 0; // @todo 3.0.150: if($processNow) $child->set('showIfSkipped', false); // https://processwire.com/talk/topic/22130-forced-10-inputfield-dependency-issues/ if(!$processNow) break; if(self::debug) $this->debugNote("$child->name ($child->label) - matched: showIf($selector)"); $processedN++; } // $selectors if(!$processNow) { if(self::debug) { $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 if(self::debug) $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'); if(self::debug) $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) if(self::debug) $this->debugNote("delayedChildren: $delayedN ($processedN processed, $unprocessedN not)"); return $savedChildren; } /** * Process input for fields with a required-if dependency * * @param WireInputData $input * @param array|Inputfield[] $delayedChildren * */ protected function processInputRequiredIf(WireInputData $input, array $delayedChildren) { // process input for any remaining delayedChildren not already processed by processInputShowIf foreach($delayedChildren as $child) { if($child->getSetting('showIfSkipped') || $child->getSetting('showIfProcessed')) continue; if(self::debug) $this->debugNote("Now Processing requiredIf delayed child: $child->name"); $child->processInput($input); } while(count($delayedChildren)) { // shift first $child off the array $child = array_shift($delayedChildren); if(!$child->getSetting('required') || $child->getSetting('requiredSkipped')) continue; // if field was not shown, then it can't be required if($child->getSetting('showIfSkipped') && !$child->getSetting('showIfProcessed')) { $child->set('requiredSkipped', true); continue; } $required = true; $selectorString = $child->getSetting('requiredIf'); if(strlen($selectorString)) { if(self::debug) $this->debugNote("requiredIf selector: $selectorString"); $selectors = $this->wire(new Selectors($selectorString)); foreach($selectors as $selector) { $fields = is_array($selector->field) ? $selector->field : array($selector->field); foreach($fields as $name) { $matches = $this->selectorMatchesInputfield($selector, $name, 'requiredIf'); if($matches === null) continue; if($matches === false) { $required = false; break; } } // foreach($fields) if(!$required) break; } // foreach($selectors) } // if(strlen($selectorString)) if($required) { if($child->isEmpty()) { if(self::debug) $this->debugNote("$child->name - determined that value IS required and is not present (error)"); $requiredLabel = $child->getSetting('requiredLabel'); if(empty($requiredLabel)) $requiredLabel = $this->requiredLabel; // requiredLabel from InputfieldWrapper $child->error($requiredLabel); } else { if(self::debug) $this->debugNote("$child->name - determined that value IS required and is populated (good)"); } } else { if(self::debug) $this->debugNote("$child->name - determined that value is not required"); $child->set('requiredSkipped', true); } } } /** * Does the selector match the given Inputfield name? * * @param Selector $selector * @param string $name Name of Inputfield * @param string $debugNote Optional qualifier note for debugging * @return bool|null Returns true|false if match determined, or NULL if $name is not present in form * */ protected function selectorMatchesInputfield(Selector $selector, $name, $debugNote = '') { $subfield = ''; if(strpos($name, '.')) list($name, $subfield) = explode('.', $name); // get the inputfield that $child has a dependency on $inputfield = $this->getChildByName($name); // if field is not present in this form, we assume a blank value for it if(!$inputfield) { if($name != 'collapsed') { $this->error("Warning ($debugNote): dependency field '$name' is not present in this form.", Notice::debug); } return null; } $value = $inputfield->attr('value'); $value2 = null; $matches = false; if($subfield == 'count') { $value = wireCount($value); if(self::debug) $this->debugNote("Actual count ($debugNote): $value"); } if(is_object($value)) $value = "$value"; if($inputfield instanceof InputfieldSelect && $subfield != 'count') { $allowCheckLabels = false; // allow for match by field labels, in addition to field values $options = $inputfield->getOptions(); // determine if selector values are referring to a value or a label foreach($selector->values as $selectorValue) { // if value in selector matches a known option 'label' then we allow use of labels $key = array_search($selectorValue, $options); if(self::debug) $this->debugNote("OPTIONS ($debugNote): Searching for label '$selectorValue' in " . print_r($options, true)); if($key !== false) { $allowCheckLabels = true; if(self::debug) $this->debugNote("OPTIONS ($debugNote): Found '$selectorValue' so allowing label check"); break; } else { if(self::debug) $this->debugNote("OPTIONS ($debugNote): Did not find '$selectorValue'"); } } if($allowCheckLabels) { if($inputfield instanceof InputfieldHasArrayValue) { $value2 = array(); // matching of labels rather than values foreach($value as $k => $v) { if(isset($options[$v])) $value2[] = $options[$v]; } if(empty($value2)) $value2 = null; } else { if(isset($options[$value])) $value2 = $options[$value]; } } } if($selector->matches($value)) { if(self::debug) $this->debugNote("Selector ($debugNote) matched value \"$selector\" (field=$name, value=" . print_r($value, true) . ")"); $matches = true; } else if($value2 !== null && $selector->matches($value2)) { if(self::debug) { $this->debugNote("Selector ($debugNote) did NOT match value \"$selector\" (field=$name, value=" . print_r($value, true) . ")"); $this->debugNote("Selector ($debugNote) matched label \"$selector\" (field=$name, label=" . print_r($value2, true) . ")"); } $matches = true; } else { if(self::debug) $this->debugNote("Selector ($debugNote) failed to match \"$selector\" (field=$name, value=" . print_r($value, true) . ")"); } return $matches; } /** * Is this form submitted? * * This should only be called after the form has been completely built and submit buttons added to it. * When using this option it is preferable (though not required) that your form has been given a name attribute. * Optionally provide the name (or instance) of the submit button you want to check. * * - Returns boolean false if not submitted. * - Returns boolean true if submitted, but submit button not known or not used. * - Returns clicked/submitted InputfieldSubmit object instance when known and available. * * @param string|InputfieldSubmit $submitName Name of submit button or instance of InputfieldSubmit * @return bool|InputfieldSubmit * @todo 3.0.125 * public function isSubmitted($submitName = '') { $method = strtoupper($this->attr('method')); $input = $this->wire('input'); $submit = false; $formName = $this->getAttribute('name'); $submitNames = array(); // if the current request method is not the same as the forms, exit early if(!$input->requestMethod($method)) return false; if(!empty($submitName)) { // given a specific submit button to check if($submitName instanceof InputfieldSubmit) { $submitName = $submitName->attr('name'); } if(is_string($submitName)) { $submitNames[] = $submitName; } } if(empty($submitNames)) { // no specific submit button, so check all known submit button names $submitNames = InputfieldSubmit::getSubmitNames(); } if(!empty($formName)) { // this form has a name attribute, so we can add that to our checks $key = '_' . $this->className(); $value = $method === 'GET' ? $input->get($key) : $input->post($key); if($value !== $formName) { // submitted form name does not match this form return false; } // at this point we know this form as submitted $submit = true; } // find out which submit button was clicked if possible foreach($submitNames as $name) { $value = $method === 'GET' ? $input->get($name) : $input->post($name); // if value not present in input for this submit button then skip it if($value === null) continue; // value was submitted for button name $f = $this->getChildByName($name); // if value submitted is not the same as value on the button, skip it if(!$f || $f->val() !== $value) continue; // we found our submit button $submit = $f; break; } // if submitted and CSRF protection in place, check that token is valid if($submit && $this->getSetting('protectCSRF') && $method === 'POST') { $csrf = $this->wire('session')->CSRF(); if(!$csrf->hasValidToken()) $submit = false; } return $submit ? $submit : false; } */ /** * For internal debugging purposes * * @param $note * */ protected function debugNote($note) { static $n = 0; if(self::debug) $this->message((++$n) . ". $note"); } /** * Return WireInputData provided to processInput() method or null if not yet applicable * * @return WireInputData|null * @since 3.0.171 * */ public function getInput() { return $this->_processInputData; } /** * Hook called right before form is rendered or processed * * @param string $type One of 'render' or 'process' * @since 3.0.171 * */ protected function ___renderOrProcessReady($type) { } }