~11.2 - IS-11032 Change submit button to have spinners and disabled behavior …#75
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a reusable “loading/disabled submit button” behavior across Identity Server UI templates to prevent duplicate submissions (notably on slow Chrome networks) and to show a spinner while requests are in flight.
Changes:
- Introduces a new
fragments/button-loading.vmscript to toggle.button-loading-active+disabledon submit buttons and to reset state onpageshow/ detected errors. - Updates OAuth, authenticator, and authentication-action Velocity templates to add the
button-loadingclass and wrap labels in<span>. - Converts some
<input type="submit">controls to<button type="submit">for consistent spinner behavior.
Reviewed changes
Copilot reviewed 47 out of 47 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/identity-server/templates/core/views/oauth/enter-usercode.vm | Converts submit to button with loading markup; includes button-loading fragment. |
| src/identity-server/templates/core/views/oauth/consent.vm | Converts consent submit/cancel to buttons with loading markup; includes fragment. |
| src/identity-server/templates/core/views/oauth/confirm-usercode.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/fragments/set-alias.vm | Adds loading markup and includes fragment for alias form. |
| src/identity-server/templates/core/fragments/button-loading.vm | New shared JS for submit-button loading/disable + error/back-nav reset behavior. |
| src/identity-server/templates/core/authenticator/webauthn/register/get.vm | Adds loading markup to registration buttons; includes fragment. |
| src/identity-server/templates/core/authenticator/webauthn/authenticate-device/get.vm | Adds loading markup to submit/authenticate buttons; includes fragment. |
| src/identity-server/templates/core/authenticator/totp/set-alias/index.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/authenticator/totp/register/index.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/authenticator/totp/authenticate/index.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/authenticator/totp/authenticate/enter-totp.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/authenticator/sms/register/index.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/authenticator/sms/enter-username/get.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/authenticator/sms/enter-otp/get.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/authenticator/sms/confirm/index.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/authenticator/pingfederate/authenticate/index.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/authenticator/passkeys/register/get.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/authenticator/passkeys/authenticate-device/get.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/authenticator/html-form/set-password/get.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/authenticator/html-form/reset-password/get.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/authenticator/html-form/forgot-account-id/get.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/authenticator/html-form/create-account/post.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/authenticator/html-form/create-account/get.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/authenticator/html-form/authenticate/get.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/authenticator/html-form/account-activation/set-password.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/authenticator/html-form/account-activation/request-new-activation.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/authenticator/html-form/account-activation/otp.vm | Adds loading markup and includes fragment for activate/resend OTP buttons. |
| src/identity-server/templates/core/authenticator/email/enter-username/index.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/authenticator/email/enter-otp/index.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/authenticator/duo/register/register-device.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/authenticator/duo/enter-username/index.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/authenticator/bankid/wait/index.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/authenticator/bankid/launch/index.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/authentication-action/signup/get.vm | Includes fragment (to enable loading behavior for submit buttons on this page). |
| src/identity-server/templates/core/authentication-action/reset-password/index.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/authentication-action/require-active-account/get.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/authentication-action/request-acknowledgement/index.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/authentication-action/opt-in-mfa/setup.vm | Adds loading markup (incl. spans) and includes fragment. |
| src/identity-server/templates/core/authentication-action/opt-in-mfa/setup-confirm.vm | Converts submit inputs to buttons w/ loading markup; includes fragment. |
| src/identity-server/templates/core/authentication-action/opt-in-mfa/select.vm | Adds loading markup (incl. spans) and includes fragment. |
| src/identity-server/templates/core/authentication-action/opt-in-mfa/select-to-manage.vm | Adds loading markup (incl. spans) and includes fragment. |
| src/identity-server/templates/core/authentication-action/opt-in-mfa/register-confirm.vm | Converts submit inputs to buttons w/ loading markup; includes fragment. |
| src/identity-server/templates/core/authentication-action/opt-in-mfa/recovery-codes-reset.vm | Converts submit inputs to buttons w/ loading markup; includes fragment. |
| src/identity-server/templates/core/authentication-action/opt-in-mfa/recovery-code.vm | Converts submit input and cancel button to loading markup; includes fragment. |
| src/identity-server/templates/core/authentication-action/opt-in-mfa/manage.vm | Adds loading markup (incl. spans) and includes fragment. |
| src/identity-server/templates/core/authentication-action/auto-link-account/index.vm | Adds loading markup and includes fragment. |
| src/identity-server/templates/core/authentication-action/attribute-prompt/index.vm | Adds loading markup and includes fragment. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Function to check if error elements exist | ||
| function hasErrorElements() { | ||
| return document.querySelector('.alert-danger, .is-error, .error, [class*="error"]') !== null; | ||
| } |
There was a problem hiding this comment.
hasErrorElements() uses a very broad selector ([class*="error"]) and doesn’t account for errors that already exist in the DOM before a new submit attempt. If an error container is always present (or a previous error remains visible), the observer will clear the loading state on the next unrelated DOM mutation, re-enabling buttons and undermining duplicate-submit protection. Consider narrowing the selector to known error containers and/or tracking the pre-submit error state and only clearing when the error state transitions from “no errors” to “errors shown”.
| // Stop observing after 5 seconds (successful navigation would unload the page) | ||
| setTimeout(function() { | ||
| if (errorObserver) { | ||
| errorObserver.disconnect(); | ||
| errorObserver = null; | ||
| } | ||
| }, 5000); | ||
| } |
There was a problem hiding this comment.
The error observer is forcibly disconnected after 5 seconds. If a submit triggers an async flow (eg WebAuthn) and the failure UI appears after this timeout, buttons can stay disabled/spinning with no way for the user to retry. Consider removing the timeout (navigation will unload the page anyway) or increasing it substantially and/or tying cleanup to a more reliable lifecycle event.
| // Add loading state to all submit buttons in this form | ||
| var submitButtons = form.querySelectorAll('button[type="submit"], input[type="submit"]'); | ||
|
|
||
| submitButtons.forEach(function(btn) { | ||
| if (!btn.classList.contains('button-loading-active')) { | ||
| btn.classList.add('button-loading-active'); | ||
| btn.disabled = true; | ||
| } |
There was a problem hiding this comment.
Loading state is applied to every submit control in the form (button[type=submit], input[type=submit]) regardless of whether it has the .button-loading base class. Since the spinner CSS relies on .button-loading (eg position: relative and span styling), applying only .button-loading-active can result in misplaced/unstyled spinners (and inputs won’t render ::after in many browsers). Consider limiting this to .button-loading buttons, or adding the base class programmatically when activating loading, and explicitly handling <button> elements that omit a type attribute (default submit).
|
@urre did you have a look at the AI review? Should we merge this or you want to fix anything else? |
…ing loading spinner
| attributeFilter: ['class'] | ||
| }); | ||
|
|
||
| // Stop observing after 5 seconds (successful navigation would unload the page) |
There was a problem hiding this comment.
I don't understand why observing for errors is stopped after 5 seconds ? If we have a slow connection, a response with errors can arrive after the 5 seconds have elapsed and we would not reset the buttons.
| </div> | ||
| </form> | ||
|
|
||
| #parse("fragments/button-loading") |
There was a problem hiding this comment.
Can't this be added into the default layout, to avoid having to add it in all templates ?
| } | ||
|
|
||
| // Create observer to watch for error elements being added to the DOM | ||
| errorObserver = new MutationObserver(function(mutations) { |
There was a problem hiding this comment.
Is MutationObserver compatible with all the Browser versions we support ? I checked that it should be ok, but asking just in case.
| } | ||
|
|
||
| // Create observer to watch for error elements being added to the DOM | ||
| errorObserver = new MutationObserver(function(mutations) { |
There was a problem hiding this comment.
Could this observer on the document body, based on css selectors, can have a performance impact on slow end devices ? Or is it ok, knowing that the size of our templates is rather small ?
Summary
button-loadingfragment that shows a spinner and disables submit buttons on form submission, preventing duplicate submissions when using Chrome on slow networksbutton-loadingCSS class and wrap button text in<span>elements for the spinner animation<input type="submit">elements to<button type="submit">for consistent loading behaviorMutationObserverthat reverts the loading state if validation errors appear, and apageshowlistener to handle browser back-button navigationHow it works
The
button-loading.vmfragment injects a script that:submitevents on all formsbutton-loading-activeclass and setsdisabledon submit buttonsMutationObserverto detect error elements and restore buttons if submission failspageshow(back/forward cache navigation)Demo
This gif shows that when button is clicked, it becomes disabled and has a spinner inside. This way it cannot be clicked again.