Short description of the issue
A field with a showIf dependency can be silently not saved when editing a page, because ProcessWire re-evaluates the showIf condition server-side during save and—in certain cases—cannot reconstruct the value the browser used to show/hide the field.
When you save a page, a showIf field is pulled out of the normal processing pass (InputfieldWrapper::isProcessable() queues it as a "delayed child") and is only saved if InputfieldForm::processInputShowIf() independently re-derives the condition as true, via selectorMatchesInputfield(). That method reads the dependency field's value to test the condition. This is a completely separate code path from the JavaScript that actually showed/hid the field in the editor, and when the two disagree the dependent field's input is discarded:
if(!$processNow) {
$child->set('showIfSkipped', true);
continue; // processInput() is never called -> value dropped
}
There is no error; isChanged() stays false, so ProcessPageEdit (which only writes changed fields) never saves it. Removing the showIf rule "fixes" it, which is why this looks intermittent.
Expected behavior
A field that was visible (its showIf condition was met in the editor) and edited by the user is saved, regardless of how the dependency field is rendered server-side.
Actual behavior
The dependent field's value is silently discarded—no error, nothing saved—in the two cases below, even though the value is present in the submitted POST data.
The common cases are fine: plain text/checkbox/select dependencies, chained dependencies, and fields nested in fieldsets all re-evaluate correctly server-side. The failures are limited to two situations where the server cannot reconstruct the dependency value:
- Dependency field not present in the server-side form — it lives in an un-opened AJAX tab (the admin default for multi-tab editing), a conditionally-built form, or was removed.
selectorMatchesInputfield() returns null (treated as "no match"), even though the submitted value is available.
- Dependency present but not processed — it is in a locked or
collapsedHidden state, so it retains only its originally-loaded value. If a value was nevertheless submitted for it (e.g. a readonly/hidden mirror input), the server matches the stale loaded value instead of the submitted one.
Optional: Suggestion for a possible fix
Make InputfieldForm::selectorMatchesInputfield() fall back to the actually-submitted value (via $this->getInput()) in both situations, so the server evaluates the same value the browser used. Normally-processed and chained-showIf dependencies are untouched (they keep using their processed/sanitized values). showIf is a UX feature—field editability/access is enforced separately—so deferring to the submitted value here is safe.
// In selectorMatchesInputfield(), wire/modules/Inputfield/InputfieldForm.module
// (1) dependency field not present in the form: fall back to the submitted value
$inputfield = $this->getChildByName($name);
if(!$inputfield) {
$input = $this->getInput();
$postValue = $input ? $input->$name : null;
if($postValue !== null) {
if(is_array($postValue)) $postValue = implode('|', $postValue);
return $selector->matches("$postValue");
}
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;
// (2) dependency present but in a non-processed (locked/hidden) state: prefer submitted value
static $notProcessedStates = array(
Inputfield::collapsedHidden,
Inputfield::collapsedLocked,
Inputfield::collapsedNoLocked,
Inputfield::collapsedBlankLocked,
Inputfield::collapsedYesLocked,
Inputfield::collapsedTabLocked,
);
if(in_array((int) $inputfield->getSetting('collapsed'), $notProcessedStates, true)) {
$input = $this->getInput();
$postValue = $input ? $input->$name : null;
if($postValue !== null) {
$value = is_array($postValue) && $subfield !== 'count' ? implode('|', $postValue) : $postValue;
}
}
Verified on a real install with assertions covering both bug cases plus regressions (text/checkbox/select/chained/nested, multi-value matching, and the .count subfield).
Steps to reproduce the issue
- Create two fields, e.g. a Page-reference/select/checkbox field
dep and a text field sif. Add both to a template.
- Give
sif a visibility dependency: Show this field only if dep=<value>.
- Put
dep on a separate tab in the template (so the admin loads it via AJAX), or set dep's visibility to Locked/Hidden.
- Edit a page: set
dep to the value that makes sif visible, enter a value in sif, and save without opening the tab that contains dep (or with dep locked/hidden).
- Reload the page edit:
sif is empty/unchanged—its value was silently dropped.
Setup/Environment
- ProcessWire version: 3.0.263 (dev); present in current master and earlier versions
- (Optional) PHP version: 8.5
Short description of the issue
A field with a
showIfdependency can be silently not saved when editing a page, because ProcessWire re-evaluates theshowIfcondition server-side during save and—in certain cases—cannot reconstruct the value the browser used to show/hide the field.When you save a page, a
showIffield is pulled out of the normal processing pass (InputfieldWrapper::isProcessable()queues it as a "delayed child") and is only saved ifInputfieldForm::processInputShowIf()independently re-derives the condition astrue, viaselectorMatchesInputfield(). That method reads the dependency field's value to test the condition. This is a completely separate code path from the JavaScript that actually showed/hid the field in the editor, and when the two disagree the dependent field's input is discarded:There is no error;
isChanged()stays false, soProcessPageEdit(which only writes changed fields) never saves it. Removing theshowIfrule "fixes" it, which is why this looks intermittent.Expected behavior
A field that was visible (its
showIfcondition was met in the editor) and edited by the user is saved, regardless of how the dependency field is rendered server-side.Actual behavior
The dependent field's value is silently discarded—no error, nothing saved—in the two cases below, even though the value is present in the submitted POST data.
The common cases are fine: plain text/checkbox/select dependencies, chained dependencies, and fields nested in fieldsets all re-evaluate correctly server-side. The failures are limited to two situations where the server cannot reconstruct the dependency value:
selectorMatchesInputfield()returnsnull(treated as "no match"), even though the submitted value is available.collapsedHiddenstate, so it retains only its originally-loaded value. If a value was nevertheless submitted for it (e.g. a readonly/hidden mirror input), the server matches the stale loaded value instead of the submitted one.Optional: Suggestion for a possible fix
Make
InputfieldForm::selectorMatchesInputfield()fall back to the actually-submitted value (via$this->getInput()) in both situations, so the server evaluates the same value the browser used. Normally-processed and chained-showIfdependencies are untouched (they keep using their processed/sanitized values).showIfis a UX feature—field editability/access is enforced separately—so deferring to the submitted value here is safe.Verified on a real install with assertions covering both bug cases plus regressions (text/checkbox/select/chained/nested, multi-value matching, and the
.countsubfield).Steps to reproduce the issue
depand a text fieldsif. Add both to a template.sifa visibility dependency: Show this field only ifdep=<value>.depon a separate tab in the template (so the admin loads it via AJAX), or setdep's visibility to Locked/Hidden.depto the value that makessifvisible, enter a value insif, and save without opening the tab that containsdep(or withdeplocked/hidden).sifis empty/unchanged—its value was silently dropped.Setup/Environment