From f08d3de0c868fdc37a9037825dd61a031efba9a3 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 26 Dec 2025 22:29:45 +0000 Subject: [PATCH] Add loader title hint for SPA navigation feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/RSpade/Core/Js/Rsx.js | 4 +++- app/RSpade/Core/SPA/Spa.js | 26 +++++++++++++++++++++++--- app/RSpade/man/spa.txt | 31 +++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/app/RSpade/Core/Js/Rsx.js b/app/RSpade/Core/Js/Rsx.js index 8b5816942..c05722608 100755 --- a/app/RSpade/Core/Js/Rsx.js +++ b/app/RSpade/Core/Js/Rsx.js @@ -491,9 +491,11 @@ class Rsx { } // Collect any extra parameters for query string + // Filter out internal parameters that should not appear in URLs + const internal_params = ['_loader_title_hint']; const query_params = {}; for (const key in params) { - if (!used_params[key]) { + if (!used_params[key] && !internal_params.includes(key)) { query_params[key] = params[key]; } } diff --git a/app/RSpade/Core/SPA/Spa.js b/app/RSpade/Core/SPA/Spa.js index 7eb79f372..bf329b859 100755 --- a/app/RSpade/Core/SPA/Spa.js +++ b/app/RSpade/Core/SPA/Spa.js @@ -465,7 +465,19 @@ class Spa { console_debug('Spa', 'Intercepting link click: ' + href); e.preventDefault(); - Spa.dispatch(href, { history: 'auto' }); + + // Check for loader title hint - provides immediate title feedback while page loads + const loader_title_hint = link.getAttribute('data-loader-title-hint'); + const dispatch_options = { history: 'auto' }; + + if (loader_title_hint) { + // Set document title immediately for instant feedback + document.title = loader_title_hint; + dispatch_options.loader_title_hint = loader_title_hint; + console_debug('Spa', 'Loader title hint: ' + loader_title_hint); + } + + Spa.dispatch(href, dispatch_options); } else { console_debug('Spa', 'No SPA route match, letting server handle: ' + href); } @@ -514,6 +526,7 @@ class Spa { history: options.history || 'auto', scroll: 'scroll' in options ? options.scroll : undefined, triggers: options.triggers !== false, + loader_title_hint: options.loader_title_hint || null, }; console_debug('Spa', 'Dispatching to: ' + url + ' (history: ' + opts.history + ')'); @@ -660,18 +673,25 @@ class Spa { const action_class = route_match.action_class; const action_name = action_class.name; + // Merge loader title hint into action args if provided + // This allows the action to show a placeholder title while loading + let action_args = route_match.args; + if (opts.loader_title_hint) { + action_args = { ...route_match.args, _loader_title_hint: opts.loader_title_hint }; + } + // Log successful SPA navigation console.warn('[Spa.dispatch] Executing SPA navigation', { url: url, path: parsed.path, - params: route_match.args, + params: action_args, action: action_name, layouts: target_layouts, history_mode: opts.history }); // Resolve layout chain - find divergence point and reuse matching layouts - await Spa._resolve_layout_chain(target_layouts, action_name, route_match.args, url); + await Spa._resolve_layout_chain(target_layouts, action_name, action_args, url); // Scroll Restoration #1: Immediate (after action starts) // This occurs synchronously after the action component is created diff --git a/app/RSpade/man/spa.txt b/app/RSpade/man/spa.txt index 3dbe11532..d99f2a941 100755 --- a/app/RSpade/man/spa.txt +++ b/app/RSpade/man/spa.txt @@ -523,6 +523,37 @@ NAVIGATION Spa.action.reload(); // Reload current action Spa.layout.update_nav(); // Call layout method + Loader Title Hint: + When navigating from a list to a detail page, you can provide a hint + for the page title to display while the action loads its data. This + provides immediate visual feedback instead of showing a blank or + generic title during the loading state. + + Add data-loader-title-hint attribute to links: + + + <%= contact.name %> + + + When the link is clicked: + 1. document.title is immediately set to the hint value + 2. The hint is passed to the action as this.args._loader_title_hint + 3. Action can use the hint while loading, then replace with real title + + Using the Hint in Actions: + async page_title() { + // Show hint while loading, real title when data is ready + if (this.is_loading() && this.args._loader_title_hint) { + return this.args._loader_title_hint; + } + return `Contact: ${this.data.contact.name}`; + } + + The _loader_title_hint parameter is automatically filtered from URLs + generated by Rsx.Route(), so it never appears in the browser address + bar or generated links. + SESSION VALIDATION After each SPA navigation (except initial load and back/forward), the client validates its state against the server by calling Rsx.validate_session().