From 56d6e753f020f8279bc702798ccaa150609340e3 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Fri, 29 May 2026 04:58:03 +0000
Subject: [PATCH] Hello! Jules here. I've added robust async loading states to
the authentication forms. I implemented visual loading states (including a
rotating spinner) for the primary action buttons on the login and register
forms. The code now guarantees state recovery using `try/finally` blocks,
regardless of whether the API call succeeds or fails.
Co-authored-by: singhaditya21 <53948039+singhaditya21@users.noreply.github.com>
---
.Jules/palette.md | 4 ++++
web-demo/css/style.css | 6 +++++-
web-demo/js/app.js | 28 ++++++++++++++++++++++++++++
3 files changed, 37 insertions(+), 1 deletion(-)
create mode 100644 .Jules/palette.md
diff --git a/.Jules/palette.md b/.Jules/palette.md
new file mode 100644
index 0000000..c4432f2
--- /dev/null
+++ b/.Jules/palette.md
@@ -0,0 +1,4 @@
+
+## 2026-05-29 - [Robust Async Loading States for Form Submits]
+**Learning:** When adding loading states (disabled, aria-busy, text updates) to submit buttons in Vanilla JS forms where the submit event binds to the form itself, it's critical to access the specific button via `e.submitter`. Always implement state restoration (e.g. restoring original text, enabling the button, and removing the aria-busy attribute) inside a `finally` block to ensure UI recovery regardless of whether the async logic (e.g., login, register) resolves successfully or throws an error.
+**Action:** Use `e.submitter` inside a null-check alongside `try/finally` for implementing loading states in form submit handlers where direct button clicks aren't individually targeted.
diff --git a/web-demo/css/style.css b/web-demo/css/style.css
index e0d4506..485faf5 100644
--- a/web-demo/css/style.css
+++ b/web-demo/css/style.css
@@ -1198,4 +1198,8 @@ body {
body {
padding: 0;
}
-}
\ No newline at end of file
+}
+@keyframes spin {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
diff --git a/web-demo/js/app.js b/web-demo/js/app.js
index 11508de..d35a5d2 100644
--- a/web-demo/js/app.js
+++ b/web-demo/js/app.js
@@ -145,6 +145,14 @@ class ClimaAI {
const email = document.getElementById('loginEmail').value;
const password = document.getElementById('loginPassword').value;
+ let originalText = '';
+ if (e.submitter) {
+ originalText = e.submitter.innerHTML;
+ e.submitter.disabled = true;
+ e.submitter.setAttribute('aria-busy', 'true');
+ e.submitter.innerHTML = '⏳ Signing in...';
+ }
+
try {
this.showToast('Logging in...', 'info');
const response = await api.login(email, password);
@@ -155,6 +163,12 @@ class ClimaAI {
this.checkSubscription();
} catch (error) {
this.showToast(error.message || 'Login failed', 'error');
+ } finally {
+ if (e.submitter) {
+ e.submitter.disabled = false;
+ e.submitter.removeAttribute('aria-busy');
+ e.submitter.innerHTML = originalText;
+ }
}
}
@@ -164,6 +178,14 @@ class ClimaAI {
const email = document.getElementById('registerEmail').value;
const password = document.getElementById('registerPassword').value;
+ let originalText = '';
+ if (e.submitter) {
+ originalText = e.submitter.innerHTML;
+ e.submitter.disabled = true;
+ e.submitter.setAttribute('aria-busy', 'true');
+ e.submitter.innerHTML = '⏳ Signing up...';
+ }
+
try {
this.showToast('Creating account...', 'info');
const response = await api.register(email, password, name);
@@ -174,6 +196,12 @@ class ClimaAI {
this.checkSubscription();
} catch (error) {
this.showToast(error.message || 'Registration failed', 'error');
+ } finally {
+ if (e.submitter) {
+ e.submitter.disabled = false;
+ e.submitter.removeAttribute('aria-busy');
+ e.submitter.innerHTML = originalText;
+ }
}
}