Skip to content

feat(js): dispatch validation lifecycle events - #17

Merged
66Ton99 merged 1 commit into
mainfrom
feat/issue-60-validation-events
Aug 24, 2026
Merged

66Ton99 merged 1 commit into
mainfrom
feat/issue-60-validation-events

Conversation

@66Ton99

@66Ton99 66Ton99 commented Aug 22, 2026

Copy link
Copy Markdown

What was broken

formapro/JsFormValidatorBundle#60 asks for three validation events — validating, success, failure — because the only extension point today is showErrors, and it is called both before and after validation. The reporter also saw every field get a ready class regardless of whether its check had finished.

Root cause

showErrors reports an error list, not a lifecycle state.

  • SvarohJsFormElement.validate() starts with clearErrorsRecursively(sourceId), which calls showErrors([], sourceId) on the element and every descendant, then calls showErrors again with the real errors. An empty list therefore means either "cleared, not checked yet" or "checked, valid".
  • UniqueEntity returns [] from validate() and reports its errors later, from the ajax response. So a field covered by a UniqueEntity check is told "no errors" twice before the server has answered.

The ready class the reporter mentions is not a second defect, it is this one: with the example in doc 3.12, the showErrors([]) of the clearing pass adds ready to fields that have not been validated yet and to fields whose uniqueness request is still in flight. No separate fix is possible without changing what showErrors([]) means, which would break existing user code, so it is fixed by giving the lifecycle its own signal.

One real second bug was found and fixed on the way: SvarohJsAjaxRequest.checkQueue() iterated this.callbacks without clearing it, so every queued callback was replayed on each following drain — a submit deferred behind the ajax queue re-ran on the next uniqueness check.

What changed

src/Resources/public/js/SvarohJsFormValidator.js, additive only. showErrors and onValidate are untouched and existing customizations keep working.

Element level, dispatched on the field DOM node, bubbling:

  • svaroh:validating — validate() starts on this element (disabled elements are skipped and fire nothing)
  • svaroh:success / svaroh:failure — the element ended without / with errors

Form level, dispatched on the form DOM node, once per validateRecursively() on a root element, which is what a submit triggers:

  • svaroh:form-validating, svaroh:form-success, svaroh:form-failure

detail.element is the SvarohJsFormElement. detail.errors is a flat message list for element events (same shape as the showErrors argument) and an object keyed by element id for form events (same shape as the onValidate argument). SvarohJsFormValidator.eventPrefix makes the svaroh: prefix configurable.

Async: success, failure, form-success and form-failure go through the new SvarohJsFormValidator.onAjaxIdle(), which fires straight away when ajax.queue is 0 and otherwise queues on ajax.callbacks, so the outcome is reported only after the UniqueEntity responses have landed and written their errors. Element outcome is computed from all of the element's error sources at that moment, not from the synchronous return value.

Native CustomEvent rather than callbacks

Chosen deliberately. The core SvarohJsFormValidator.js has no jQuery dependency, and jQuery's .on() receives events dispatched with dispatchEvent and copies detail onto its own event object, so the same three names work in all three entry points (SvarohJsFormValidator.js, SvarohJsFormValidatorWithJqueryInit.js, jquery.svarohjsformvalidator.js) with no bridge code in jquery.svarohjsformvalidator.js at all. Callbacks would have meant a single-slot-per-element property like showErrors, where a second listener silently replaces the first — the opposite of what an event API is for. Events also bubble, so $('form').on('svaroh:failure', 'input', ...) works, which matters here because the bundle attaches exactly one listener of its own (submit on the form) and has no field-level change pipeline. Form level events use distinct names precisely because element events bubble up to the form and would otherwise be indistinguishable there.

Docs

New page src/Resources/doc/3_18.md with the event table, the two detail shapes, jQuery and plain JS examples, the async section, and a corrected version of the marker-class example. Pointers added from 3.2, 3.11 and 3.12, plus the README index.

3_15, 3_16 and 3_17 are claimed by three sibling branches, so this page took the next free number; renumbering at merge time is expected.

Tests

nix develop -c npx jest: 34 suites, 357 tests (was 347), all green. composer test: 43 tests / 151 assertions. composer phpstan: clean.

10 tests added in SvarohJsFormValidator.test.js; 8 of them fail on main without this change. They cover element level ordering and payloads, disabled elements, bubbling, form level events on a valid and on an invalid form, no form level events for a child element, the one-shot ajax callback queue, and the async path: a real UniqueEntity constraint with a stubbed XHR asserting that only validating and form-validating have fired while the request is pending, and that failure and form-failure arrive with the uniqueness message once the response lands.

Known limitation, not addressed

sendRequest only reacts to readyState === 4 && status === 200. A failed uniqueness request leaves ajax.queue stuck above 0, so the queued callbacks never run. That already blocked the deferred submit before this change; the new events inherit the same limitation and it belongs to its own issue.

showErrors is called both when previous errors are cleared and when new
ones are rendered, so it cannot tell a started validation run from a
finished one, and it marks a field as valid while an asynchronous
constraint is still waiting for its answer.

Dispatch native CustomEvent objects instead: svaroh:validating,
svaroh:success and svaroh:failure on the element DOM node, and
svaroh:form-validating, svaroh:form-success and svaroh:form-failure on
the form DOM node. The success and failure events wait for the ajax
queue to drain so they describe the final result of the run.

Queued ajax callbacks are now taken off the queue before they run, they
were kept and replayed on every following drain.

Fixes formapro#60
@66Ton99
66Ton99 force-pushed the feat/issue-60-validation-events branch from 398114f to b6266f5 Compare August 24, 2026 19:31
@66Ton99
66Ton99 merged commit e6614ba into main Aug 24, 2026
6 checks passed
@66Ton99
66Ton99 deleted the feat/issue-60-validation-events branch August 26, 2026 06:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant