Files
rspade_system/app/RSpade/man/breadcrumbs.txt
root 29b1abc0a1 Add progressive breadcrumb resolution with caching
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-12 08:55:34 +00:00

405 lines
14 KiB
Plaintext
Executable File

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:
<Define:Breadcrumb_Nav tag="nav" aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<% 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) { %>
<li class="breadcrumb-item active" aria-current="page">
<% if (crumb.label === null) { %>
<span class="breadcrumb-placeholder">------</span>
<% } else { %>
<%= crumb.label %>
<% } %>
</li>
<% } else { %>
<li class="breadcrumb-item">
<a href="<%= crumb.url %>">
<% if (crumb.label === null) { %>
<span class="breadcrumb-placeholder">------</span>
<% } else { %>
<%= crumb.label %>
<% } %>
</a>
</li>
<% } %>
<% } %>
</ol>
</Define:Breadcrumb_Nav>
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):
<Page_Header>
<Page_Header_Left>
<Page_Title>Edit Contact</Page_Title>
<Breadcrumb>
<Breadcrumb_Item $href="...">Dashboard</Breadcrumb_Item>
<Breadcrumb_Item $href="...">Contacts</Breadcrumb_Item>
<Breadcrumb_Item $active=true>Edit</Breadcrumb_Item>
</Breadcrumb>
</Page_Header_Left>
</Page_Header>
New Pattern (action methods):
// Remove from template, keep only:
<Page_Header>
<Page_Header_Right>
<!-- action buttons -->
</Page_Header_Right>
</Page_Header>
// 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