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:
@@ -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`);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user