🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
405 lines
14 KiB
Plaintext
Executable File
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
|