Implement JQHTML function cache ID system and fix bundle compilation Implement underscore prefix for system tables Fix JS syntax linter to support decorators and grant exception to Task system SPA: Update planning docs and wishlists with remaining features SPA: Document Navigation API abandonment and future enhancements Implement SPA browser integration with History API (Phase 1) Convert contacts view page to SPA action Convert clients pages to SPA actions and document conversion procedure SPA: Merge GET parameters and update documentation Implement SPA route URL generation in JavaScript and PHP Implement SPA bootstrap controller architecture Add SPA routing manual page (rsx:man spa) Add SPA routing documentation to CLAUDE.md Phase 4 Complete: Client-side SPA routing implementation Update get_routes() consumers for unified route structure Complete SPA Phase 3: PHP-side route type detection and is_spa flag Restore unified routes structure and Manifest_Query class Refactor route indexing and add SPA infrastructure Phase 3 Complete: SPA route registration in manifest Implement SPA Phase 2: Extract router code and test decorators Rename Jqhtml_Component to Component and complete SPA foundation setup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
689 lines
25 KiB
Plaintext
Executable File
689 lines
25 KiB
Plaintext
Executable File
SPA(3) RSX Framework Manual SPA(3)
|
|
|
|
NAME
|
|
spa - Single Page Application routing for authenticated areas
|
|
|
|
SYNOPSIS
|
|
PHP Bootstrap Controller (one per SPA module):
|
|
|
|
class Frontend_Spa_Controller extends Rsx_Controller_Abstract
|
|
{
|
|
#[SPA]
|
|
#[Auth('Permission::authenticated()')]
|
|
public static function index(Request $request, array $params = [])
|
|
{
|
|
return rsx_view(SPA);
|
|
}
|
|
}
|
|
|
|
JavaScript Action (many per module):
|
|
|
|
@route('/contacts')
|
|
@layout('Frontend_Layout')
|
|
@spa('Frontend_Spa_Controller::index')
|
|
class Contacts_Index_Action extends Spa_Action {
|
|
async on_load() {
|
|
this.data.contacts = await Frontend_Contacts_Controller.datagrid_fetch();
|
|
}
|
|
}
|
|
|
|
DESCRIPTION
|
|
The RSX SPA system provides client-side routing for authenticated application
|
|
areas, enabling navigation without page reloads. Unlike traditional Laravel
|
|
views where each navigation triggers a full page load, SPA modules bootstrap
|
|
once and handle all subsequent navigation client-side through JavaScript
|
|
actions.
|
|
|
|
This approach is fundamentally different from traditional server-side routing:
|
|
- Traditional: Every navigation loads new HTML from server
|
|
- SPA: First request bootstraps application, subsequent navigation is client-side
|
|
|
|
Key differences from Laravel:
|
|
- Laravel: Full page reload per navigation, routes in routes/ files
|
|
- RSX: Bootstrap once, routes defined in JavaScript with @route() decorator
|
|
|
|
- Laravel: Separate frontend frameworks (Vue, React) require build tooling
|
|
- RSX: Integrated JQHTML component system, no external build required
|
|
|
|
- Laravel: API routes separate from view routes
|
|
- RSX: Controllers provide Ajax endpoints, actions consume them
|
|
|
|
Benefits:
|
|
- No page reloads after initial bootstrap
|
|
- Persistent layout across navigation
|
|
- Automatic browser history integration
|
|
- Same Rsx::Route() syntax for both traditional and SPA routes
|
|
- No frontend build tooling required
|
|
|
|
When to use:
|
|
- Authenticated areas (dashboards, admin panels)
|
|
- Applications with frequent navigation
|
|
- Features requiring persistent state
|
|
|
|
When to avoid:
|
|
- Public pages requiring SEO
|
|
- Simple static content
|
|
- Forms without complex interactions
|
|
|
|
SPA ARCHITECTURE
|
|
SPA Module Structure:
|
|
An SPA module consists of six component types:
|
|
|
|
1. PHP Bootstrap Controller (ONE per feature/bundle)
|
|
- Single entry point marked with #[SPA]
|
|
- Performs authentication check with failure/redirect
|
|
- Renders SPA bootstrap layout
|
|
- Referenced by all actions via @spa() decorator
|
|
- One #[SPA] per feature/bundle (e.g., /app/frontend, /app/root, /app/login)
|
|
- Bundles segregate code for security and performance:
|
|
* Save bandwidth by loading only needed features
|
|
* Reduce processing time by smaller bundle sizes
|
|
* Protect confidential code (e.g., root admin from unauthorized users)
|
|
- Typically one #[SPA] per feature at rsx/app/(feature)/(feature)_spa_controller::index
|
|
|
|
2. Feature Controllers (Ajax Endpoints Only)
|
|
- Provide data via #[Ajax_Endpoint] methods
|
|
- No #[SPA] or #[Route] methods
|
|
- Called by actions to fetch/save data
|
|
|
|
3. JavaScript Actions (MANY per module)
|
|
- Represent individual pages/routes
|
|
- Define route patterns with @route()
|
|
- Load data in on_load() lifecycle method
|
|
- Access URL parameters via this.args
|
|
|
|
4. Action Templates (.jqhtml)
|
|
- Render action content
|
|
- Standard JQHTML component templates
|
|
- Replace traditional Blade views
|
|
|
|
5. Layout Template (.jqhtml)
|
|
- Persistent wrapper around actions
|
|
- Must have element with $id="content"
|
|
- Persists across action navigation
|
|
|
|
6. Layout Class (.js)
|
|
- Extends Spa_Layout
|
|
- Optional on_action() hook for navigation tracking
|
|
|
|
Execution Flow:
|
|
First Request:
|
|
1. User navigates to SPA route (e.g., /contacts)
|
|
2. Dispatcher calls bootstrap controller
|
|
3. Bootstrap returns rsx_view(SPA) with window.rsxapp.is_spa = true
|
|
4. Client JavaScript discovers all actions via manifest
|
|
5. Router matches URL to action class
|
|
6. Creates layout on <body>
|
|
7. Creates action inside layout $id="content" area
|
|
|
|
Subsequent Navigation:
|
|
1. User clicks link or calls Spa.dispatch()
|
|
2. Router matches URL to action class
|
|
3. Destroys old action, creates new action
|
|
4. Layout persists - no re-render
|
|
5. No server request, no page reload
|
|
|
|
BOOTSTRAP CONTROLLER
|
|
Creating SPA Entry Point:
|
|
// /rsx/app/frontend/Frontend_Spa_Controller.php
|
|
class Frontend_Spa_Controller extends Rsx_Controller_Abstract
|
|
{
|
|
#[SPA]
|
|
#[Auth('Permission::authenticated()')]
|
|
public static function index(Request $request, array $params = [])
|
|
{
|
|
return rsx_view(SPA);
|
|
}
|
|
}
|
|
|
|
Key Points:
|
|
- One bootstrap controller per feature/bundle
|
|
- #[SPA] attribute marks as SPA entry point
|
|
- #[Auth] performs server-side authentication with failure/redirect
|
|
- Must return rsx_view(SPA) - special constant
|
|
- All actions in the same bundle reference this via @spa() decorator
|
|
|
|
Bundle Segregation:
|
|
- Separate bundles for different features: /app/frontend, /app/root, /app/login
|
|
- Each bundle has its own #[SPA] bootstrap
|
|
- Benefits:
|
|
* Bandwidth: Users only download code for authorized features
|
|
* Performance: Smaller bundles = faster load times
|
|
* Security: Root admin code never sent to unauthorized users
|
|
- Example: Frontend users never receive root admin JavaScript/CSS
|
|
|
|
Location:
|
|
- Place at feature root: /rsx/app/{feature}/
|
|
- Not in subdirectories
|
|
- One per feature/bundle
|
|
- Naming: {Feature}_Spa_Controller::index
|
|
|
|
Multiple SPA Bootstraps:
|
|
Different features can have separate SPA bootstraps:
|
|
- /app/frontend/Frontend_Spa_Controller::index (regular users)
|
|
- /app/root/Root_Spa_Controller::index (root admins)
|
|
- /app/login/Login_Spa_Controller::index (authentication flow)
|
|
|
|
Each bootstrap controls access to its feature's actions via #[Auth].
|
|
|
|
FEATURE CONTROLLERS
|
|
Ajax Endpoint Pattern:
|
|
Feature controllers provide data endpoints only, no routes or SPA entry:
|
|
|
|
// /rsx/app/frontend/contacts/frontend_contacts_controller.php
|
|
class Frontend_Contacts_Controller extends Rsx_Controller_Abstract
|
|
{
|
|
#[Ajax_Endpoint]
|
|
public static function datagrid_fetch(Request $request, array $params = []): array
|
|
{
|
|
return Contacts_DataGrid::fetch($params);
|
|
}
|
|
|
|
#[Ajax_Endpoint]
|
|
public static function save(Request $request, array $params = [])
|
|
{
|
|
// Validation and save logic
|
|
$contact->save();
|
|
|
|
Flash_Alert::success('Contact saved successfully');
|
|
|
|
return [
|
|
'contact_id' => $contact->id,
|
|
'redirect' => Rsx::Route('Contacts_View_Action', $contact->id),
|
|
];
|
|
}
|
|
}
|
|
|
|
Guidelines:
|
|
- All methods are #[Ajax_Endpoint]
|
|
- No #[Route] methods in SPA feature controllers
|
|
- No #[SPA] methods (only in bootstrap controller)
|
|
- Return arrays/data, not views
|
|
- Use Flash_Alert with redirects for success messages
|
|
|
|
See Also:
|
|
ajax_error_handling(3) for error patterns
|
|
controller(3) for #[Ajax_Endpoint] details
|
|
|
|
JAVASCRIPT ACTIONS
|
|
Creating Actions:
|
|
// /rsx/app/frontend/contacts/Contacts_Index_Action.js
|
|
@route('/contacts')
|
|
@layout('Frontend_Layout')
|
|
@spa('Frontend_Spa_Controller::index')
|
|
class Contacts_Index_Action extends Spa_Action {
|
|
async on_load() {
|
|
this.data.contacts = await Frontend_Contacts_Controller.datagrid_fetch();
|
|
}
|
|
|
|
on_create() {
|
|
// Component initialization
|
|
}
|
|
|
|
on_ready() {
|
|
// DOM is ready, setup event handlers
|
|
this.$id('search').on('input', () => this.reload());
|
|
}
|
|
}
|
|
|
|
Decorator Syntax:
|
|
@route(pattern)
|
|
URL pattern with optional :param segments
|
|
Example: '/contacts/:id', '/users/:id/posts/:post_id'
|
|
|
|
@layout(class_name)
|
|
Layout class to render within
|
|
Example: 'Frontend_Layout', 'Dashboard_Layout'
|
|
|
|
@spa(controller::method)
|
|
References bootstrap controller
|
|
Example: 'Frontend_Spa_Controller::index'
|
|
|
|
@title(page_title)
|
|
Browser page title for this action (optional, clears if not present)
|
|
Alternatively, set dynamically in on_ready() via document.title = "value"
|
|
Example: @title('Contacts - RSX')
|
|
|
|
URL Parameters:
|
|
Parameters from route pattern and query string available in this.args:
|
|
|
|
// URL: /contacts/123?tab=history
|
|
@route('/contacts/:id')
|
|
class Contacts_View_Action extends Spa_Action {
|
|
on_create() {
|
|
console.log(this.args.id); // "123"
|
|
console.log(this.args.tab); // "history"
|
|
}
|
|
}
|
|
|
|
Lifecycle Methods:
|
|
on_create() Component construction, setup this.state
|
|
on_load() Fetch data, populate this.data (read-only this.args)
|
|
on_render() Template rendering
|
|
on_ready() DOM ready, setup event handlers
|
|
|
|
See Also:
|
|
jqhtml(3) for complete lifecycle documentation
|
|
|
|
ACTION TEMPLATES
|
|
Creating Templates:
|
|
Action templates are standard JQHTML components:
|
|
|
|
<!-- /rsx/app/frontend/contacts/Contacts_Index_Action.jqhtml -->
|
|
<Define:Contacts_Index_Action tag="main">
|
|
<h1>Contacts</h1>
|
|
|
|
<div class="contacts-list">
|
|
<% for (let contact of this.data.contacts) { %>
|
|
<div class="contact-item">
|
|
<a href="<%= Rsx.Route('Contacts_View_Action', {id: contact.id}) %>">
|
|
<%= contact.name %>
|
|
</a>
|
|
</div>
|
|
<% } %>
|
|
</div>
|
|
</Define:Contacts_Index_Action>
|
|
|
|
Key Points:
|
|
- Use Rsx.Route() for all URLs, never hardcode
|
|
- Access data via this.data (loaded in on_load)
|
|
- Use standard JQHTML template syntax
|
|
- Can reference other components
|
|
|
|
LAYOUTS
|
|
Layout Template:
|
|
Layout provides persistent wrapper around actions:
|
|
|
|
<!-- /rsx/app/frontend/Frontend_Layout.jqhtml -->
|
|
<Define:Frontend_Layout>
|
|
<div class="frontend-layout">
|
|
<header class="mb-3">
|
|
<h1>My App</h1>
|
|
<nav>
|
|
<a href="<%= Rsx.Route('Contacts_Index_Action') %>">Contacts</a>
|
|
<a href="<%= Rsx.Route('Projects_Index_Action') %>">Projects</a>
|
|
</nav>
|
|
</header>
|
|
|
|
<main $id="content">
|
|
<!-- Actions render here -->
|
|
</main>
|
|
|
|
<footer class="mt-3">
|
|
<p>© 2024 My Company</p>
|
|
</footer>
|
|
</div>
|
|
</Define:Frontend_Layout>
|
|
|
|
Requirements:
|
|
- Must have element with $id="content"
|
|
- Content area is where actions render
|
|
- Layout persists across navigation
|
|
|
|
Layout Class:
|
|
Optional JavaScript class for navigation tracking:
|
|
|
|
// /rsx/app/frontend/Frontend_Layout.js
|
|
class Frontend_Layout extends Spa_Layout {
|
|
on_action(url, action_name, args) {
|
|
// Called AFTER action component created and booted
|
|
// Called BEFORE action reaches on_ready()
|
|
// Can immediately access this.action properties
|
|
|
|
// Update active nav highlighting
|
|
this.$('.nav-link').removeClass('active');
|
|
this.$(`[data-action="${action_name}"]`).addClass('active');
|
|
|
|
// If you need to wait for action's full lifecycle:
|
|
// await this.action.ready();
|
|
}
|
|
}
|
|
|
|
on_action() Lifecycle Timing:
|
|
- Called after action component is created and booted
|
|
- Called before action reaches on_ready()
|
|
- Can immediately access this.action properties
|
|
- Use await this.action.ready() to wait for action's full loading
|
|
|
|
URL GENERATION
|
|
CRITICAL: All URLs must use Rsx::Route() or Rsx.Route(). Raw URLs like
|
|
"/contacts" will produce errors.
|
|
|
|
Signature:
|
|
Rsx::Route($action, $params = null)
|
|
Rsx.Route(action, params = null)
|
|
|
|
Where:
|
|
- $action: Controller class, SPA action, or "Class::method"
|
|
Defaults to 'index' method if no :: present
|
|
- $params: Integer sets 'id', array/object provides named params
|
|
|
|
PHP Syntax:
|
|
// Works for both controller routes and SPA actions
|
|
Rsx::Route('Contacts_Index_Action') // /contacts
|
|
Rsx::Route('Contacts_View_Action', 123) // /contacts/123
|
|
|
|
JavaScript Syntax:
|
|
// Works for both controller routes and SPA actions
|
|
Rsx.Route('Contacts_Index_Action') // /contacts
|
|
Rsx.Route('Contacts_View_Action', 123) // /contacts/123
|
|
|
|
Automatic Detection:
|
|
Rsx::Route() automatically detects whether the class is:
|
|
- PHP controller with #[Route] attribute
|
|
- JavaScript SPA action with @route() decorator
|
|
|
|
Query Parameters:
|
|
Extra parameters become query string:
|
|
|
|
Rsx::Route('Contacts_Index_Action', ['filter' => 'active', 'sort' => 'name'])
|
|
// Result: /contacts?filter=active&sort=name
|
|
|
|
NAVIGATION
|
|
Automatic Browser Integration:
|
|
- Clicking links to SPA routes triggers client-side navigation
|
|
- Browser back/forward buttons handled without page reload
|
|
- URL updates in address bar via pushState()
|
|
- Links to external or non-SPA routes perform full page load
|
|
|
|
Programmatic Navigation:
|
|
Spa.dispatch(url)
|
|
Navigate to URL programmatically
|
|
If URL is part of current SPA, uses client-side routing
|
|
If URL is external or different SPA, performs full page load
|
|
|
|
Example:
|
|
// Navigate within SPA
|
|
Spa.dispatch('/contacts/123');
|
|
|
|
// Navigate to external URL (full page load)
|
|
Spa.dispatch('https://example.com');
|
|
|
|
Use Spa.dispatch() instead of window.location for all navigation.
|
|
|
|
Accessing Active Components:
|
|
Spa.layout Reference to current layout component instance
|
|
Spa.action Reference to current action component instance
|
|
|
|
Example:
|
|
// From anywhere in SPA context
|
|
Spa.action.reload(); // Reload current action
|
|
Spa.layout.update_nav(); // Call layout method
|
|
|
|
FILE ORGANIZATION
|
|
Standard SPA Module Structure:
|
|
/rsx/app/frontend/
|
|
|-- Frontend_Spa_Controller.php # Bootstrap controller
|
|
|-- Frontend_Layout.js # Layout class
|
|
|-- Frontend_Layout.jqhtml # Layout template
|
|
|-- frontend_bundle.php # Bundle definition
|
|
|
|
|
|-- contacts/
|
|
| |-- frontend_contacts_controller.php # Ajax endpoints
|
|
| |-- Contacts_Index_Action.js # /contacts
|
|
| |-- Contacts_Index_Action.jqhtml
|
|
| |-- Contacts_View_Action.js # /contacts/:id
|
|
| `-- Contacts_View_Action.jqhtml
|
|
|
|
|
`-- projects/
|
|
|-- frontend_projects_controller.php
|
|
|-- Projects_Index_Action.js
|
|
`-- Projects_Index_Action.jqhtml
|
|
|
|
Naming Conventions:
|
|
- Bootstrap controller: {Module}_Spa_Controller.php
|
|
- Feature controllers: {feature}_{module}_controller.php
|
|
- Actions: {Feature}_{Action}_Action.js
|
|
- Templates: {Feature}_{Action}_Action.jqhtml
|
|
- Layout: {Module}_Layout.js and .jqhtml
|
|
|
|
EXAMPLES
|
|
Complete Contacts Module:
|
|
|
|
1. Bootstrap Controller:
|
|
// /rsx/app/frontend/Frontend_Spa_Controller.php
|
|
class Frontend_Spa_Controller extends Rsx_Controller_Abstract
|
|
{
|
|
#[SPA]
|
|
#[Auth('Permission::authenticated()')]
|
|
public static function index(Request $request, array $params = [])
|
|
{
|
|
return rsx_view(SPA);
|
|
}
|
|
}
|
|
|
|
2. Feature Controller:
|
|
// /rsx/app/frontend/contacts/frontend_contacts_controller.php
|
|
class Frontend_Contacts_Controller extends Rsx_Controller_Abstract
|
|
{
|
|
#[Ajax_Endpoint]
|
|
public static function fetch_all(Request $request, array $params = []): array
|
|
{
|
|
$contacts = Contact_Model::query()
|
|
->where('is_active', 1)
|
|
->orderBy('name')
|
|
->get();
|
|
|
|
return $contacts->toArray();
|
|
}
|
|
|
|
#[Ajax_Endpoint]
|
|
public static function save(Request $request, array $params = [])
|
|
{
|
|
$contact = Contact_Model::findOrNew($params['id'] ?? null);
|
|
$contact->fill($params);
|
|
$contact->save();
|
|
|
|
Flash_Alert::success('Contact saved');
|
|
|
|
return [
|
|
'contact_id' => $contact->id,
|
|
'redirect' => Rsx::Route('Contacts_View_Action', $contact->id),
|
|
];
|
|
}
|
|
}
|
|
|
|
3. List Action:
|
|
// /rsx/app/frontend/contacts/Contacts_Index_Action.js
|
|
@route('/contacts')
|
|
@layout('Frontend_Layout')
|
|
@spa('Frontend_Spa_Controller::index')
|
|
class Contacts_Index_Action extends Spa_Action {
|
|
async on_load() {
|
|
this.data.contacts = await Frontend_Contacts_Controller.fetch_all();
|
|
}
|
|
}
|
|
|
|
4. List Template:
|
|
<!-- /rsx/app/frontend/contacts/Contacts_Index_Action.jqhtml -->
|
|
<Define:Contacts_Index_Action tag="main">
|
|
<h1>Contacts</h1>
|
|
<% for (let contact of this.data.contacts) { %>
|
|
<div>
|
|
<a href="<%= Rsx.Route('Contacts_View_Action', {id: contact.id}) %>">
|
|
<%= contact.name %>
|
|
</a>
|
|
</div>
|
|
<% } %>
|
|
</Define:Contacts_Index_Action>
|
|
|
|
5. View Action with Parameters:
|
|
// /rsx/app/frontend/contacts/Contacts_View_Action.js
|
|
@route('/contacts/:id')
|
|
@layout('Frontend_Layout')
|
|
@spa('Frontend_Spa_Controller::index')
|
|
class Contacts_View_Action extends Spa_Action {
|
|
async on_load() {
|
|
const contact_id = this.args.id;
|
|
this.data.contact = await Contact_Model.fetch(contact_id);
|
|
}
|
|
}
|
|
|
|
6. Layout:
|
|
<!-- /rsx/app/frontend/Frontend_Layout.jqhtml -->
|
|
<Define:Frontend_Layout>
|
|
<div class="app">
|
|
<header>
|
|
<nav>
|
|
<a href="<%= Rsx.Route('Contacts_Index_Action') %>">Contacts</a>
|
|
</nav>
|
|
</header>
|
|
<main $id="content"></main>
|
|
</div>
|
|
</Define:Frontend_Layout>
|
|
|
|
SPA VS TRADITIONAL ROUTES
|
|
Architecture Comparison:
|
|
|
|
Traditional Route:
|
|
Every navigation:
|
|
1. Browser sends HTTP request to server
|
|
2. Controller executes, queries database
|
|
3. Blade template renders full HTML page
|
|
4. Server sends complete HTML to browser
|
|
5. Browser parses and renders new page
|
|
6. JavaScript re-initializes
|
|
7. User sees page flash/reload
|
|
|
|
SPA Route:
|
|
First navigation:
|
|
1. Browser sends HTTP request to server
|
|
2. Bootstrap controller renders SPA shell
|
|
3. JavaScript initializes, discovers actions
|
|
4. Router matches URL to action
|
|
5. Action loads data via Ajax
|
|
6. Action renders in persistent layout
|
|
|
|
Subsequent navigation:
|
|
1. Router matches URL to action (client-side)
|
|
2. New action loads data via Ajax
|
|
3. New action renders in existing layout
|
|
4. No server request, no page reload
|
|
5. Layout persists (header/nav/footer stay)
|
|
|
|
Code Comparison:
|
|
|
|
Traditional:
|
|
// Controller
|
|
#[Route('/contacts')]
|
|
public static function index(Request $request, array $params = []) {
|
|
$contacts = Contact_Model::all();
|
|
return rsx_view('Contacts_List', ['contacts' => $contacts]);
|
|
}
|
|
|
|
// Blade view
|
|
@extends('layout')
|
|
@section('content')
|
|
<h1>Contacts</h1>
|
|
@foreach($contacts as $contact)
|
|
<div>{{ $contact->name }}</div>
|
|
@endforeach
|
|
@endsection
|
|
|
|
SPA:
|
|
// Bootstrap (once)
|
|
#[SPA]
|
|
public static function index(Request $request, array $params = []) {
|
|
return rsx_view(SPA);
|
|
}
|
|
|
|
// Ajax endpoint
|
|
#[Ajax_Endpoint]
|
|
public static function fetch_all(Request $request, array $params = []): array {
|
|
return Contact_Model::all()->toArray();
|
|
}
|
|
|
|
// Action
|
|
@route('/contacts')
|
|
@spa('Frontend_Spa_Controller::index')
|
|
class Contacts_Index_Action extends Spa_Action {
|
|
async on_load() {
|
|
this.data.contacts = await Frontend_Contacts_Controller.fetch_all();
|
|
}
|
|
}
|
|
|
|
// Template
|
|
<Define:Contacts_Index_Action>
|
|
<h1>Contacts</h1>
|
|
<% for (let contact of this.data.contacts) { %>
|
|
<div><%= contact.name %></div>
|
|
<% } %>
|
|
</Define:Contacts_Index_Action>
|
|
|
|
TROUBLESHOOTING
|
|
SPA Not Initializing:
|
|
- Verify bootstrap controller has #[SPA] attribute
|
|
- Check controller returns rsx_view(SPA)
|
|
- Ensure bundle includes SPA directory
|
|
- Verify window.rsxapp.is_spa is true in HTML
|
|
|
|
Action Not Found:
|
|
- Check @route() decorator matches URL pattern
|
|
- Verify action extends Spa_Action
|
|
- Ensure action file included in bundle
|
|
- Run: php artisan rsx:manifest:build
|
|
|
|
Navigation Not Working:
|
|
- Verify using Rsx.Route() not hardcoded URLs
|
|
- Check @spa() decorator references correct bootstrap controller
|
|
- Ensure layout has $id="content" element
|
|
- Test Spa.dispatch() directly
|
|
|
|
Layout Not Persisting:
|
|
- Verify all actions in module use same @layout()
|
|
- Check layout template has $id="content"
|
|
- Ensure not mixing SPA and traditional routes
|
|
|
|
this.args Empty:
|
|
- Check route pattern includes :param for dynamic segments
|
|
- Verify URL matches route pattern exactly
|
|
- Remember query params (?key=value) also in this.args
|
|
|
|
Data Not Loading:
|
|
- Verify Ajax endpoint has #[Ajax_Endpoint]
|
|
- Check endpoint returns array, not view
|
|
- Ensure await used in on_load() for async calls
|
|
- Verify controller included in bundle
|
|
|
|
COMMON PATTERNS
|
|
Conditional Navigation:
|
|
// Navigate based on condition
|
|
if (user.is_admin) {
|
|
Spa.dispatch(Rsx.Route('Admin_Dashboard_Action'));
|
|
} else {
|
|
Spa.dispatch(Rsx.Route('User_Dashboard_Action'));
|
|
}
|
|
|
|
Reloading Current Action:
|
|
// Refresh current action data
|
|
Spa.action.reload();
|
|
|
|
Form Submission with Redirect:
|
|
async on_submit() {
|
|
const data = this.vals();
|
|
const result = await Controller.save(data);
|
|
|
|
if (result.errors) {
|
|
Form_Utils.apply_form_errors(this.$, result.errors);
|
|
return;
|
|
}
|
|
|
|
// Flash alert set server-side, redirect to view
|
|
Spa.dispatch(result.redirect);
|
|
}
|
|
|
|
Multiple Layouts:
|
|
// Different layouts for different areas
|
|
@layout('Admin_Layout')
|
|
class Admin_Users_Action extends Spa_Action { }
|
|
|
|
@layout('Frontend_Layout')
|
|
class Frontend_Contacts_Action extends Spa_Action { }
|
|
|
|
SEE ALSO
|
|
controller(3) - Controller patterns and Ajax endpoints
|
|
jqhtml(3) - Component lifecycle and templates
|
|
routing(3) - URL generation and route patterns
|
|
modals(3) - Modal dialogs in SPA context
|
|
ajax_error_handling(3) - Error handling patterns
|