🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
336 lines
11 KiB
Plaintext
Executable File
336 lines
11 KiB
Plaintext
Executable File
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:
|
|
|
|
<Breadcrumb_Nav $crumbs="<%= JSON.stringify(chain) %>" />
|
|
|
|
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:
|
|
<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">
|
|
<%= crumb.label %>
|
|
</li>
|
|
<% } else { %>
|
|
<li class="breadcrumb-item">
|
|
<a href="<%= crumb.url %>"><%= 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
|
|
|
|
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):
|
|
<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
|