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