From 365a4a8e6d8eb7a6a327848c6fe7d647ce12009c Mon Sep 17 00:00:00 2001 From: Ton Sharp <45160296+66Ton99@users.noreply.github.com> Date: Thu, 27 Aug 2026 22:47:05 +0300 Subject: [PATCH 1/2] fix(js): initialize a model added after the document is ready "addModel" defers registration to "onDocumentReady", which only added a "DOMContentLoaded" listener. A form fragment that an application fetches and injects after the initial load runs its "addModel" call when that event has already been dispatched, so the listener was never called and the form kept no validator at all. The callback now runs immediately once "document.readyState" is past "loading", and still waits for the event while the document is parsing. --- src/Resources/doc/2_3.md | 6 +++- .../public/js/SvarohJsFormValidator.js | 10 ++++++ .../public/js/SvarohJsFormValidator.test.js | 35 +++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/Resources/doc/2_3.md b/src/Resources/doc/2_3.md index 6caf9bfb..9c677b01 100644 --- a/src/Resources/doc/2_3.md +++ b/src/Resources/doc/2_3.md @@ -48,7 +48,11 @@ init_js_validation(form = null, onLoad = true, wrapped = true) - `form`: By default, all queued forms are processed. Pass a form name or `FormView` instance to process only one form. - `onLoad`: When `true`, validation models are initialized on document ready. - Pass `false` when you want to initialize them yourself. + Pass `false` when you want to initialize them yourself. A model rendered into + a document that is already past parsing - a form fragment an application + fetched and injected after the initial load - is initialized right away, + because the document ready event it would otherwise wait for has already been + dispatched. - `wrapped`: When `true`, the generated code is wrapped in a `` tag. diff --git a/src/Resources/public/js/SvarohJsFormValidator.js b/src/Resources/public/js/SvarohJsFormValidator.js index 8e4adc08..b8c1328a 100644 --- a/src/Resources/public/js/SvarohJsFormValidator.js +++ b/src/Resources/public/js/SvarohJsFormValidator.js @@ -678,6 +678,16 @@ var SvarohJsFormValidator = new function () { }; this.onDocumentReady = function (callback) { + // A model added after the document was parsed - a form fragment that a + // single page application fetched and injected - would wait here for a + // "DOMContentLoaded" that has already been dispatched, so the callback + // runs right away once the document is past parsing + if (document.readyState && 'loading' !== document.readyState) { + callback(); + + return; + } + var addListener = document.addEventListener || document.attachEvent; var removeListener = document.removeEventListener || document.detachEvent; var eventName = document.addEventListener ? "DOMContentLoaded" : "onreadystatechange"; diff --git a/src/Resources/public/js/SvarohJsFormValidator.test.js b/src/Resources/public/js/SvarohJsFormValidator.test.js index c36ba02a..2b838da3 100644 --- a/src/Resources/public/js/SvarohJsFormValidator.test.js +++ b/src/Resources/public/js/SvarohJsFormValidator.test.js @@ -1276,6 +1276,41 @@ describe('SvarohJsFormValidator model registration', () => { expect(Object.keys(window.SvarohJsFormValidator.forms)).toEqual(['profile']); expect(window.SvarohJsFormValidator.forms.profile.domNode).toBe(document.getElementById('profile')); }); + + // A single page application injects a form long after "DOMContentLoaded", + // and a listener for that event would never be called again + test('registers a model of a form injected after the document is ready', () => { + document.body.innerHTML = '
'; + + window.SvarohJsFormValidator.addModel(buildModel('profile', 'profile', { + email: buildModel('profile_email', 'profile[email]'), + })); + + expect(Object.keys(window.SvarohJsFormValidator.forms)).toEqual(['profile']); + expect(window.SvarohJsFormValidator.forms.profile.domNode).toBe(document.getElementById('profile')); + }); + + test('still waits for the document while it is being parsed', () => { + Object.defineProperty(document, 'readyState', { + configurable: true, + get: () => 'loading', + }); + + try { + document.body.innerHTML = '
'; + window.SvarohJsFormValidator.addModel(buildModel('profile', 'profile', { + email: buildModel('profile_email', 'profile[email]'), + })); + + expect(window.SvarohJsFormValidator.forms.profile).toBeUndefined(); + + document.dispatchEvent(new Event('DOMContentLoaded')); + + expect(window.SvarohJsFormValidator.forms.profile.domNode).toBe(document.getElementById('profile')); + } finally { + delete document.readyState; + } + }); }); describe('SvarohJsFormValidator property paths', () => { From b7c40c69a06bc62d8c861ab97203545153b07d46 Mon Sep 17 00:00:00 2001 From: 66Ton99 <66ton99@gmail.com> Date: Fri, 28 Aug 2026 00:22:41 +0300 Subject: [PATCH 2/2] fix(js): wait for the document while its deferred scripts still run "onDocumentReady" ran the callback for every state past "loading", but "interactive" means the document is parsed while its deferred scripts, and the "js_validator_config()" one of them may carry, still run before "DOMContentLoaded". Only "complete" stands for an event that is really gone, so the call a model added with "onLoad = false" defers keeps waiting for the configuration it turns the native UI off with. The same condition is what old IE needs, where "interactive" does not mean the document can be walked yet. The listener the other branch adds is removable again. It passed the event it received to "removeEventListener", which identifies no listener, so every call left one behind, and the fallback ran its callback on every "readystatechange" instead of the last one. A single page application that swaps a rendered form for a new one left the element of the node it removed in "formInstances", and every reopened modal or revisited wizard step added one more. A render that is no longer in the document is now dropped when the same form is initialized again. Co-Authored-By: Claude Opus 5 --- AGENTS.md | 2 +- src/Resources/doc/2_3.md | 33 ++++- src/Resources/doc/3_20.md | 5 + .../public/js/SvarohJsFormValidator.js | 63 ++++++-- .../public/js/SvarohJsFormValidator.test.js | 134 +++++++++++++++--- 5 files changed, 206 insertions(+), 31 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 56fa49ea..34f42a0d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,7 +29,7 @@ Last verified in the Nix shell on PHP 8.5.6 / Node 24.16.0: - `composer test`: 95 tests, 268 assertions. - `composer phpstan`: no errors. - `composer coverage`: PHP line coverage ~97%, threshold `80%`. -- `npm run test:unit`: Jest 608 tests. +- `npm run test:unit`: Jest 615 tests. - `npm run test:coverage`: JavaScript line coverage ~95%, threshold `80%`. - `npm test`: the above plus the Cypress e2e suite, 24 tests. diff --git a/src/Resources/doc/2_3.md b/src/Resources/doc/2_3.md index 9c677b01..5f05cffd 100644 --- a/src/Resources/doc/2_3.md +++ b/src/Resources/doc/2_3.md @@ -49,14 +49,43 @@ init_js_validation(form = null, onLoad = true, wrapped = true) `FormView` instance to process only one form. - `onLoad`: When `true`, validation models are initialized on document ready. Pass `false` when you want to initialize them yourself. A model rendered into - a document that is already past parsing - a form fragment an application + a document that already finished loading - a form fragment an application fetched and injected after the initial load - is initialized right away, because the document ready event it would otherwise wait for has already been dispatched. - `wrapped`: When `true`, the generated code is wrapped in a `` tag. -For example, to initialize validation after an Ajax request: +#### Forms loaded after the page + +A form fragment the application fetches and injects - a modal that loads its +form, a step of a wizard, a single page CRUD - carries the `addModel()` call +the bundle rendered with it, and that call initializes the form as soon as it +runs. The default `onLoad` is enough, there is nothing to pass: + +```twig +{{ form(user) }} +{{ init_js_validation(user) }} +``` + +The one thing the application has to take care of is that the call runs at +all. `innerHTML` does not execute a `