Fix detached action redirect loops, abstract Spa_Action detection, jqhtml update

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-12-10 23:46:50 +00:00
parent 2f2cf41139
commit 3c25b2ff80
87 changed files with 419 additions and 72 deletions

View File

@@ -1001,11 +1001,14 @@ class Spa {
const $detached = $('<div>');
// Instantiate the action on the detached element
// This triggers the full component lifecycle: on_create -> render -> on_render -> on_load -> on_ready
$detached.component(action_name, args);
// 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);
const action = $detached.component();
// Wait for the action to be fully ready (including on_load completion)
// Wait for on_load to complete (data fetching)
await action.ready();
console_debug('Spa', `load_detached_action: ${action_name} ready`);

View File

@@ -78,4 +78,71 @@ class Spa_Action extends Component {
const url = this.url(params);
Spa.dispatch(url);
}
// =========================================================================
// Page Title & Breadcrumb System
// =========================================================================
/**
* Page title displayed in the header/title area
*
* Override this in every action to provide a meaningful page title.
* For entity pages, include entity details (e.g., "Contact: John Smith C001")
*
* @returns {Promise<string>} The page title
*/
async page_title() {
return '(title not set)';
}
/**
* Breadcrumb label for this action
*
* Used when this action appears as a parent in another action's breadcrumb chain.
* For entity pages (viewing a specific user, contact, etc.), return the entity name.
* For list/index pages, return the section name.
*
* Default: Returns page_title()
*
* @returns {Promise<string>} The breadcrumb label
*/
async breadcrumb_label() {
return await this.page_title();
}
/**
* Breadcrumb label when this action is the active/last crumb
*
* Use this to show a descriptive action name instead of the entity name
* when the entity name is already visible in the page title above.
*
* Example: For a user profile view page:
* - page_title() = "User Profile: John Smith U001"
* - breadcrumb_label() = "John Smith" (for when it's a parent)
* - breadcrumb_label_active() = "View User Profile" (avoids redundancy with title)
*
* Default: Returns breadcrumb_label()
*
* @returns {Promise<string>} The active breadcrumb label
*/
async breadcrumb_label_active() {
return await this.breadcrumb_label();
}
/**
* Parent action URL for breadcrumb chain
*
* Return the URL of the parent action using Rsx.Route().
* Return null if this action is a root (no parent breadcrumb).
*
* Example:
* async breadcrumb_parent() {
* return Rsx.Route('Settings_Users_Action');
* }
*
* @returns {Promise<string|null>} Parent URL or null if root
*/
async breadcrumb_parent() {
return null;
}
}

View File

@@ -350,6 +350,38 @@ PERFORMANCE CONSIDERATIONS
Excessive decorator usage can increase bundle size.
Consider if simple function calls would be more appropriate.
DECORATOR PARAMETER RESTRICTIONS
Class Name Identifiers Prohibited (JS-DECORATOR-IDENT-01):
Decorator parameters MUST NOT use class name identifiers (bare class
references). The framework cannot guarantee class definition order in
compiled bundle output, so referencing a class in a decorator parameter
may fail if that class hasn't been defined yet.
// WRONG - class identifier as parameter
@some_decorator(User_Model)
class My_Action extends Spa_Action { }
// CORRECT - use string literal instead
@some_decorator('User_Model')
class My_Action extends Spa_Action { }
Why This Matters:
Bundle compilation orders classes based on inheritance hierarchies
(extends chains) and other factors, but NOT based on decorator
parameter dependencies. If Class A's decorator references Class B,
there's no guarantee Class B will be defined before Class A's
decorator is evaluated.
Resolution:
Pass class names as strings and resolve at runtime if needed:
// In decorator implementation or consuming code
const cls = Manifest.get_class_by_name('User_Model');
Enforcement:
This restriction is enforced by the JS-DECORATOR-IDENT-01 code quality
rule, which runs at manifest build time for immediate feedback.
SEE ALSO
bundle_api - Bundle loading and compilation
manifest_api - Build-time validation system

View File

@@ -779,6 +779,54 @@ COMMON PATTERNS
@layout('Frontend_Layout')
class Frontend_Contacts_Action extends Spa_Action { }
DETACHED ACTION LOADING
Spa.load_detached_action() loads an action without affecting the live SPA state.
The action is instantiated on a detached DOM element, runs its full lifecycle
(including on_load), and returns the component instance for inspection.
Use cases:
- Extracting action metadata (titles, breadcrumbs) for navigation UI
- Pre-fetching action data before navigation
- Inspecting action state without rendering it visibly
Basic Usage:
const action = await Spa.load_detached_action('/contacts/123');
if (action) {
const title = action.get_title?.() ?? action.constructor.name;
const breadcrumbs = action.get_breadcrumbs?.();
console.log('Page title:', title);
// IMPORTANT: Clean up when done to prevent memory leaks
action.stop();
}
With Cached Data:
// Skip network request if cached data available
const action = await Spa.load_detached_action('/contacts/123', {
use_cached_data: true
});
Extra Arguments:
// Pass additional args merged with URL-extracted params
const action = await Spa.load_detached_action('/contacts/123', {
some_option: true,
use_cached_data: true
});
What It Does NOT Affect:
- Spa.action (current live action remains unchanged)
- Spa.layout (current live layout remains unchanged)
- Spa.route / Spa.params (current route state unchanged)
- Browser history
- The visible DOM
Returns:
- Fully-initialized Spa_Action instance if route matches
- null if no route matches the URL
IMPORTANT: The caller MUST call action.stop() when done with the detached
action to clean up event listeners and prevent memory leaks.
SEE ALSO
controller(3) - Controller patterns and Ajax endpoints
jqhtml(3) - Component lifecycle and templates