BREADCRUMBS(3) RSX Framework Manual BREADCRUMBS(3) NAME breadcrumbs - Progressive breadcrumb navigation with caching SYNOPSIS Layout Integration: class My_Spa_Layout extends Spa_Layout { on_action(url, action_name, args) { if (this._breadcrumb_cancel) this._breadcrumb_cancel(); this._breadcrumb_cancel = Rsx_Breadcrumb_Resolver.stream( this.action, url, (data) => this._render_breadcrumbs(data) ); } } 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'); } } DESCRIPTION The RSX breadcrumb system provides progressive breadcrumb resolution with caching for instant SPA navigation. Breadcrumbs render immediately using cached data while validating against live action data in the background. Key characteristics: - Instant render from cache when all chain links are cached - Progressive label updates for uncached pages (shows "------" placeholders) - Active page always validates against live action data - In-memory cache persists for session (cleared on page reload) Resolution Phases: 1. Immediate Cached Render - If ALL chain links cached, render instantly 2. Active Action Resolution - Await live action's breadcrumb methods 3. Chain Walk - Walk parents using cache or detached action loads 4. Render Chain - Emit chain structure (may have null labels) 5. Progressive Updates - Update labels as they resolve CORE API Rsx_Breadcrumb_Resolver.stream(action, url, callback) Streams breadcrumb data as it becomes available. Parameters: action - The active SPA action instance url - Current URL (pathname + search) callback - Called with breadcrumb data on each update Returns: Function - Call to cancel streaming (e.g., on navigation) Callback receives: { title: "Page Title" | null, chain: [ { url: "/path", label: "Label" | null, is_active: false, resolved: true }, { url: "/path/sub", label: null, is_active: true, resolved: false } ], fully_resolved: true | false } When label is null, the Breadcrumb_Nav component displays "------" in muted text as a placeholder. 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 uses Rsx_Breadcrumb_Resolver.stream() to progressively render breadcrumbs as data becomes available. Complete Example: class My_Spa_Layout extends Spa_Layout { on_create() { this._breadcrumb_cancel = null; } on_action(url, action_name, args) { // Cancel any previous breadcrumb stream if (this._breadcrumb_cancel) { this._breadcrumb_cancel(); } // Stream breadcrumbs progressively this._breadcrumb_cancel = Rsx_Breadcrumb_Resolver.stream( this.action, url, (data) => this._render_breadcrumbs(data) ); } _render_breadcrumbs(data) { // Update title (null = still loading) if (data.title !== null) { this.$sid('header_title').html(data.title); } // Update breadcrumbs this.$sid('header_breadcrumbs').component('Breadcrumb_Nav', { crumbs: data.chain }); } } The callback is called multiple times: - First with cached data (if full cache hit) - Then with validated active action data - Then progressively as parent labels resolve BREADCRUMB_NAV COMPONENT Template: 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 CACHING The breadcrumb resolver maintains an in-memory cache keyed by URL: Cache Structure: { label, label_active, parent_url } Cache Behavior: - Parent links: Use cache if available, skip detached action load - Active link: Show cached value immediately, then validate with fresh data from live action - Fresh values overwrite cached values Cache Invalidation: - Visiting a page updates cache for that URL only - Window reload clears entire cache - No TTL - cache persists for session duration - Parent links are never re-validated (only active page validates) Example Timeline (first visit to /contacts/123): t=0ms Cache miss for /contacts/123, skip Phase 1 t=50ms Active action resolves, update cache t=51ms Walk chain: /contacts is NOT cached, load detached t=100ms /contacts resolves, cache and render chain t=101ms fully_resolved: true Example Timeline (second visit to /contacts/123): t=0ms Full cache hit, render immediately t=1ms User sees breadcrumbs instantly t=50ms Active action validates (values unchanged) t=51ms No re-render needed PERFORMANCE CONSIDERATIONS Progressive Loading: The resolver doesn't wait for all data before rendering. It emits updates as each piece of data becomes available, showing "------" placeholders for pending labels. Detached Action Cleanup: The resolver automatically stops detached actions after extracting breadcrumb data to 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