Issue #3632737: Final submit validation rebuilds the entire form once per wizard page

Closes #3632737

Fixes #3632737.

Problem

With forward navigation, the final submit calls WebformNavigationHelper::validateAllPages(), which validates each accessible page in its own programmatic submission of the form. Webform hides every page except the current one by setting #access to FALSE on it (WebformWizardPage::hidePage()), so each page needs a separate build. On a webform with N pages, the final submit builds the complete form N + 1 times, running every form alter and webform handler each time. On large webforms this makes the final submit very slow.

Changes

  • Opt-in single pass validation. A new third-party setting, "Validate all pages in a single pass on final submit" (single_pass_validation, off by default), validates every accessible page in one build of the form. The handler shows the hidden pages in that build, restoring each element's access with the same checks WebformElementBase::prepare() applies (disabled elements, #private, access rules and handler accessElement()), so elements the user has no access to are still not validated.
  • Validation build flag. Every form built to validate the submission, in either mode, has the webformnavigation_validating form state value set (WebformNavigationHelper::VALIDATING). The navigation handler uses it to skip its page visit logging and error messages, which are only needed for display, and other modules can use it to skip display-only work in their form alters.
  • Pages for page-scoped handlers. In single pass mode, the webformnavigation_validate_pages form state value (WebformNavigationHelper::VALIDATE_PAGES) lists the pages being validated. Handlers that only validate the current page can validate each listed page instead; the README shows the pattern.
  • Error logging for every validated page. validateAllPages() now resets and logs the errors of every validated page once, instead of relying on each inner build to log its own page. This is required for single pass mode, and it means an error fixed from another page (e.g. a field made required by a #states condition on a later page) is cleared in both modes. logPageErrors() takes an optional list of pages to reset.
  • The accessible pages are computed once instead of once per page.

Without the new setting, each page is still validated in its own build, and the same errors are reported as before.

API changes

  • WebformNavigationHelper::__construct() takes the webform element manager (@plugin.manager.webform.element) as a new last argument.
  • New WebformNavigationHelper::showPages(), and the VALIDATING and VALIDATE_PAGES constants.
  • WebformNavigationHelper::logPageErrors() has a new optional $pages argument.

Behavior change when the setting is enabled

Webform handlers' validateForm() runs once, with every page shown, instead of once per page. Handlers that only validate $webform_submission->getCurrentPage() need to use webformnavigation_validate_pages before the setting is enabled. This is described in the setting's description and the README.

Tests

Adds ValidateAllPagesTest and a small webformnavigation_test module that records each validation build and the pages it lists. Each test runs with and without single pass validation.

Test Checks
testValidateAllPages Skipping to the last page and submitting reports the errors of each skipped page, does not validate a #private element the user has no access to, and completes once the errors are fixed. One build per page makes 3 validation builds; single pass makes 1, listing all 3 pages.
testErrorFixedFromAnotherPageIsCleared A page two field required by a checkbox on page three: unchecking it on page three clears the page two error without revisiting page two.

ThirdPartySettingsBooleanTest also covers the new checkbox being stored as a boolean.

Edited by Ryan McVeigh

Merge request reports

Loading
Loading