diff --git a/app/RSpade/Core/SPA/Spa.js b/app/RSpade/Core/SPA/Spa.js
index e8ba36d8b..07898bb19 100755
--- a/app/RSpade/Core/SPA/Spa.js
+++ b/app/RSpade/Core/SPA/Spa.js
@@ -993,7 +993,13 @@ class Spa {
const action_name = action_class.name;
// Merge URL args with extra_args (extra_args take precedence)
- const args = { ...route_match.args, ...extra_args };
+ // Include skip_render_and_ready to prevent side effects from on_ready()
+ // (e.g., Spa.dispatch() which would cause redirect loops)
+ const args = {
+ ...route_match.args,
+ ...extra_args,
+ skip_render_and_ready: true
+ };
console_debug('Spa', `load_detached_action: Loading ${action_name} with args:`, args);
@@ -1001,11 +1007,7 @@ class Spa {
const $detached = $('
');
// Instantiate the action on the detached element
- // Skip render and on_ready phases - detached actions are for data extraction only
- // (e.g., getting title/breadcrumbs). Running on_ready() could trigger side effects
- // like Spa.dispatch() which would cause redirect loops.
- const options = { skip_render_and_ready: true };
- $detached.component(action_name, args, options);
+ $detached.component(action_name, args);
const action = $detached.component();
// Wait for on_load to complete (data fetching)
diff --git a/app/RSpade/man/breadcrumbs.txt b/app/RSpade/man/breadcrumbs.txt
new file mode 100755
index 000000000..368ba7b54
--- /dev/null
+++ b/app/RSpade/man/breadcrumbs.txt
@@ -0,0 +1,335 @@
+BREADCRUMBS(3) RSX Framework Manual BREADCRUMBS(3)
+
+NAME
+ breadcrumbs - Layout-managed breadcrumb navigation system for SPA actions
+
+SYNOPSIS
+ Action Methods:
+
+ class Contacts_View_Action extends Spa_Action {
+ async page_title() { return this.data.contact.name; }
+ async breadcrumb_label() { return this.data.contact.name; }
+ async breadcrumb_label_active() { return 'View Contact Details'; }
+ async breadcrumb_parent() {
+ return Rsx.Route('Contacts_Index_Action');
+ }
+ }
+
+ Template Component:
+
+
" />
+
+DESCRIPTION
+ The RSX breadcrumb system provides hierarchical navigation through action
+ methods that define breadcrumb metadata. Unlike traditional breadcrumb
+ systems where each page hardcodes its full breadcrumb path, RSX uses
+ parent URL traversal to automatically build the breadcrumb chain.
+
+ Key characteristics:
+ - Actions define breadcrumb behavior through async methods
+ - Layout walks up the parent chain using Spa.load_detached_action()
+ - Each parent contributes its breadcrumb_label() to the chain
+ - Active page uses breadcrumb_label_active() for descriptive text
+ - Document title set from page_title() method
+
+ This approach provides:
+ - Dynamic breadcrumb content based on loaded data
+ - Automatic chain building without hardcoding paths
+ - Consistent breadcrumb behavior across all SPA actions
+ - Descriptive active breadcrumb text for root pages
+
+ACTION METHODS
+ page_title()
+ Returns the page title for document.title. Called once when action
+ renders. Used for browser tab title.
+
+ async page_title() {
+ return 'Contacts'; // Root page
+ }
+
+ async page_title() {
+ return this.data.contact.name; // Detail page
+ }
+
+ async page_title() {
+ return this.data.is_edit
+ ? `Edit: ${this.data.contact.name}`
+ : 'New Contact'; // Add/Edit page
+ }
+
+ breadcrumb_label()
+ Returns the label shown when this action appears as a parent in
+ another action's breadcrumb chain. Typically returns the entity
+ name or short identifier.
+
+ async breadcrumb_label() {
+ return this.data.contact.name;
+ }
+
+ async breadcrumb_label() {
+ return 'Projects';
+ }
+
+ Not needed for pages that are never parents (leaf nodes like edit
+ forms). Default returns page_title() if not defined.
+
+ breadcrumb_label_active()
+ Returns the label shown for the current active page. This can be
+ more descriptive than breadcrumb_label() since it's not constrained
+ by breadcrumb width.
+
+ // Root pages - descriptive text
+ async breadcrumb_label_active() {
+ return 'Manage your contact database';
+ }
+
+ // Detail pages - entity identifier
+ async breadcrumb_label_active() {
+ return `View ${this.data.contact.name}`;
+ }
+
+ // Edit pages - action context
+ async breadcrumb_label_active() {
+ return this.data.is_edit ? 'Edit Contact' : 'New Contact';
+ }
+
+ breadcrumb_parent()
+ Returns the URL of the parent action for building the breadcrumb
+ chain. The layout walks up this chain calling breadcrumb_label()
+ on each parent until reaching null.
+
+ // Root pages - no parent
+ async breadcrumb_parent() {
+ return null;
+ }
+
+ // View page - parent is list
+ async breadcrumb_parent() {
+ return Rsx.Route('Contacts_Index_Action');
+ }
+
+ // Edit page - parent depends on context
+ async breadcrumb_parent() {
+ if (this.data.is_edit) {
+ return Rsx.Route('Contacts_View_Action', {
+ id: this.args.id
+ });
+ }
+ return Rsx.Route('Contacts_Index_Action');
+ }
+
+LAYOUT INTEGRATION
+ The layout is responsible for building the breadcrumb chain and rendering
+ it. This happens in the layout's on_action() method.
+
+ Building the Chain:
+ async _update_header_from_action() {
+ const chain = [];
+
+ // Start with current action's active label
+ chain.push({
+ label: await this.action.breadcrumb_label_active(),
+ url: null // Active item has no link
+ });
+
+ // Walk up parent chain
+ let parent_url = await this.action.breadcrumb_parent();
+ while (parent_url) {
+ const parent = await Spa.load_detached_action(parent_url, {
+ use_cached_data: true
+ });
+
+ if (!parent) break;
+
+ chain.unshift({
+ label: await parent.breadcrumb_label(),
+ url: parent_url
+ });
+
+ parent_url = await parent.breadcrumb_parent();
+ parent.stop(); // Clean up detached action
+ }
+
+ this._set_header({
+ title: await this.action.page_title(),
+ breadcrumbs: chain
+ });
+ }
+
+ Calling from on_action():
+ async on_action(url, action_name, args) {
+ await this._update_header_from_action();
+ }
+
+BREADCRUMB_NAV COMPONENT
+ Template:
+
+
+ <% for (let i = 0; i < this.args.crumbs.length; i++) {
+ const crumb = this.args.crumbs[i];
+ const is_last = i === this.args.crumbs.length - 1;
+ %>
+ <% if (is_last || !crumb.url) { %>
+ -
+ <%= crumb.label %>
+
+ <% } else { %>
+ -
+ <%= crumb.label %>
+
+ <% } %>
+ <% } %>
+
+
+
+ Uses Bootstrap 5 breadcrumb classes for consistent styling.
+
+PATTERNS BY PAGE TYPE
+ Index/Root Pages:
+ Actions at the top of a hierarchy with no parent.
+
+ class Contacts_Index_Action extends Spa_Action {
+ async page_title() { return 'Contacts'; }
+ async breadcrumb_label() { return 'Contacts'; }
+ async breadcrumb_label_active() {
+ return 'Manage your contact database';
+ }
+ async breadcrumb_parent() { return null; }
+ }
+
+ Result: Contacts / Manage your contact database
+ (single breadcrumb with descriptive text)
+
+ View/Detail Pages:
+ Actions showing a single record with a parent list page.
+
+ class Contacts_View_Action extends Spa_Action {
+ async page_title() { return this.data.contact.name; }
+ async breadcrumb_label() { return this.data.contact.name; }
+ async breadcrumb_label_active() { return 'View Contact'; }
+ async breadcrumb_parent() {
+ return Rsx.Route('Contacts_Index_Action');
+ }
+ }
+
+ Result: Contacts > John Smith / View Contact
+
+ Edit Pages:
+ Actions for editing existing or creating new records.
+
+ class Contacts_Edit_Action extends Spa_Action {
+ async page_title() {
+ if (this.data.is_edit) {
+ return `Edit: ${this.data.contact.name}`;
+ }
+ return 'New Contact';
+ }
+
+ async breadcrumb_label_active() {
+ return this.data.is_edit ? 'Edit Contact' : 'New Contact';
+ }
+
+ async breadcrumb_parent() {
+ if (this.data.is_edit) {
+ return Rsx.Route('Contacts_View_Action', {
+ id: this.args.id
+ });
+ }
+ return Rsx.Route('Contacts_Index_Action');
+ }
+ }
+
+ Edit result: Contacts > John Smith > View Contact / Edit Contact
+ Add result: Contacts / New Contact
+
+ Nested Pages (Sublayouts):
+ Actions within sublayouts like settings sections.
+
+ class Settings_Profile_Display_Action extends Spa_Action {
+ async page_title() { return 'Display Preferences'; }
+ async breadcrumb_label() { return 'Display Preferences'; }
+ async breadcrumb_label_active() {
+ return 'Configure display settings';
+ }
+ async breadcrumb_parent() {
+ return Rsx.Route('Dashboard_Index_Action');
+ }
+ }
+
+ Result: Dashboard > Display Preferences / Configure display settings
+
+DOCUMENT TITLE
+ The layout sets document.title using page_title():
+
+ document.title = 'RSX - ' + await this.action.page_title();
+
+ Format: "AppName - PageTitle"
+
+ Examples:
+ - RSX - Contacts
+ - RSX - John Smith
+ - RSX - Edit: John Smith
+
+PERFORMANCE CONSIDERATIONS
+ Parent Chain Loading:
+ Spa.load_detached_action() with use_cached_data: true uses cached
+ data from previous visits when available, minimizing network requests.
+
+ When visiting Contacts > John Smith > Edit Contact for the first time:
+ 1. Edit page loads with fresh data (required)
+ 2. View page loads detached with use_cached_data (may hit cache)
+ 3. Index page loads detached with use_cached_data (may hit cache)
+
+ Subsequent breadcrumb renders reuse cached data.
+
+ Stopping Detached Actions:
+ Always call parent.stop() after extracting breadcrumb data to clean
+ up event listeners and prevent memory leaks.
+
+DEFAULT METHODS
+ If an action doesn't define breadcrumb methods, defaults apply:
+
+ page_title() Returns class name (e.g., "Contacts_View_Action")
+ breadcrumb_label() Returns page_title()
+ breadcrumb_label_active() Returns page_title()
+ breadcrumb_parent() Returns null (no parent)
+
+ Override only the methods you need. Most actions need at minimum:
+ - page_title()
+ - breadcrumb_label_active() (for descriptive root page text)
+ - breadcrumb_parent() (for non-root pages)
+
+MIGRATION FROM HARDCODED BREADCRUMBS
+ If converting from inline breadcrumb components:
+
+ Old Pattern (template-based):
+
+
+ Edit Contact
+
+ Dashboard
+ Contacts
+ Edit
+
+
+
+
+ New Pattern (action methods):
+ // Remove from template, keep only:
+
+
+
+
+
+
+ // Add to action class:
+ async page_title() { return 'Edit Contact'; }
+ async breadcrumb_label_active() { return 'Edit Contact'; }
+ async breadcrumb_parent() {
+ return Rsx.Route('Contacts_View_Action', { id: this.args.id });
+ }
+
+SEE ALSO
+ spa(3) - SPA routing and layouts
+ spa(3) DETACHED ACTION LOADING - Spa.load_detached_action() details
+ spa(3) SUBLAYOUTS - Nested layout handling