Remove unused blade settings pages not linked from UI Convert remaining frontend pages to SPA actions Convert settings user_settings and general to SPA actions Convert settings profile pages to SPA actions Convert contacts and projects add/edit pages to SPA actions Convert clients add/edit page to SPA action with loading pattern Refactor component scoped IDs from $id to $sid Fix jqhtml comment syntax and implement universal error component system Update all application code to use new unified error system Remove all backwards compatibility - unified error system complete Phase 5: Remove old response classes Phase 3-4: Ajax response handler sends new format, old helpers deprecated Phase 2: Add client-side unified error foundation Phase 1: Add server-side unified error foundation Add unified Ajax error response system with constants 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
231 lines
8.1 KiB
Plaintext
Executable File
231 lines
8.1 KiB
Plaintext
Executable File
VIEW_ACTION_PATTERNS(3) RSX Manual VIEW_ACTION_PATTERNS(3)
|
|
|
|
NAME
|
|
view_action_patterns - Best practices for SPA view actions with
|
|
dynamic content loading
|
|
|
|
SYNOPSIS
|
|
Recommended pattern for view/detail pages that load a single record:
|
|
|
|
Action Class (Feature_View_Action.js):
|
|
@route('/feature/view/:id')
|
|
@layout('Frontend_Spa_Layout')
|
|
@spa('Frontend_Spa_Controller::index')
|
|
@title('Feature Details')
|
|
class Feature_View_Action extends Spa_Action {
|
|
on_create() {
|
|
this.data.record = { name: '' };
|
|
this.data.error_data = null;
|
|
this.data.loading = true;
|
|
}
|
|
|
|
async on_load() {
|
|
try {
|
|
this.data.record = await Controller.get({
|
|
id: this.args.id
|
|
});
|
|
} catch (e) {
|
|
this.data.error_data = e;
|
|
}
|
|
this.data.loading = false;
|
|
}
|
|
}
|
|
|
|
Template (Feature_View_Action.jqhtml):
|
|
<Define:Feature_View_Action>
|
|
<Page>
|
|
<% if (this.data.loading) { %>
|
|
<Loading_Spinner $message="Loading..." />
|
|
<% } else if (this.data.error_data) { %>
|
|
<Universal_Error_Page_Component
|
|
$error_data="<%= this.data.error_data %>"
|
|
$record_type="Feature"
|
|
$back_label="Go back to Features"
|
|
$back_url="<%= Rsx.Route('Feature_Index_Action') %>"
|
|
/>
|
|
<% } else { %>
|
|
<!-- Normal content here -->
|
|
<% } %>
|
|
</Page>
|
|
</Define:Feature_View_Action>
|
|
|
|
DESCRIPTION
|
|
This document describes the recommended pattern for building view/detail
|
|
pages in RSpade SPA applications. The pattern provides:
|
|
|
|
- Loading state with spinner during data fetch
|
|
- Automatic error handling for all Ajax error types
|
|
- Clean three-state template (loading → error → content)
|
|
- Consistent user experience across all view pages
|
|
|
|
This is an opinionated best practice from the RSpade starter template.
|
|
Developers are free to implement alternatives, but this pattern handles
|
|
common cases well and provides a consistent structure.
|
|
|
|
THE THREE-STATE PATTERN
|
|
Every view action has exactly three possible states:
|
|
|
|
1. LOADING - Data is being fetched
|
|
- Show Loading_Spinner component
|
|
- Prevents flash of empty/broken content
|
|
- User knows something is happening
|
|
|
|
2. ERROR - Data fetch failed
|
|
- Show Universal_Error_Page_Component
|
|
- Automatically routes to appropriate error display
|
|
- Handles not found, unauthorized, server errors, etc.
|
|
|
|
3. SUCCESS - Data loaded successfully
|
|
- Show normal page content
|
|
- Safe to access this.data.record properties
|
|
|
|
Template structure:
|
|
<% if (this.data.loading) { %>
|
|
<!-- State 1: Loading -->
|
|
<% } else if (this.data.error_data) { %>
|
|
<!-- State 2: Error -->
|
|
<% } else { %>
|
|
<!-- State 3: Success -->
|
|
<% } %>
|
|
|
|
ACTION CLASS STRUCTURE
|
|
on_create() - Initialize defaults
|
|
on_create() {
|
|
// Stub object prevents undefined errors during first render
|
|
this.data.record = { name: '' };
|
|
|
|
// Error holder - null means no error
|
|
this.data.error_data = null;
|
|
|
|
// Start in loading state
|
|
this.data.loading = true;
|
|
}
|
|
|
|
Key points:
|
|
- Initialize this.data.record with empty stub matching expected shape
|
|
- Prevents "cannot read property of undefined" during initial render
|
|
- Set loading=true so spinner shows immediately
|
|
|
|
async on_load() - Fetch data with error handling
|
|
async on_load() {
|
|
try {
|
|
this.data.record = await Controller.get({
|
|
id: this.args.id
|
|
});
|
|
} catch (e) {
|
|
this.data.error_data = e;
|
|
}
|
|
this.data.loading = false;
|
|
}
|
|
|
|
Key points:
|
|
- Wrap Ajax call in try/catch
|
|
- Store caught error in this.data.error_data (not re-throw)
|
|
- Always set loading=false in finally or after try/catch
|
|
- Error object has .code, .message, .metadata from Ajax system
|
|
|
|
UNIVERSAL ERROR COMPONENT
|
|
The Universal_Error_Page_Component automatically displays the right
|
|
error UI based on error.code:
|
|
|
|
Required arguments:
|
|
$error_data - The error object from catch block
|
|
$record_type - Human name: "Project", "Contact", "User"
|
|
$back_label - Button text: "Go back to Projects"
|
|
$back_url - Where back button navigates
|
|
|
|
Error types handled:
|
|
Ajax.ERROR_NOT_FOUND → "Project Not Found" with back button
|
|
Ajax.ERROR_UNAUTHORIZED → "Access Denied" message
|
|
Ajax.ERROR_AUTH_REQUIRED → "Login Required" with login button
|
|
Ajax.ERROR_SERVER → "Server Error" with retry button
|
|
Ajax.ERROR_NETWORK → "Connection Error" with retry button
|
|
Ajax.ERROR_VALIDATION → Field error list
|
|
Ajax.ERROR_GENERIC → Generic error with retry
|
|
|
|
HANDLING SPECIFIC ERRORS DIFFERENTLY
|
|
Sometimes you need custom handling for specific error types while
|
|
letting others go to the universal handler:
|
|
|
|
async on_load() {
|
|
try {
|
|
this.data.record = await Controller.get({id: this.args.id});
|
|
} catch (e) {
|
|
if (e.code === Ajax.ERROR_NOT_FOUND) {
|
|
// Custom handling: redirect to create page
|
|
Spa.dispatch(Rsx.Route('Feature_Create_Action'));
|
|
return;
|
|
}
|
|
// All other errors: use universal handler
|
|
this.data.error_data = e;
|
|
}
|
|
this.data.loading = false;
|
|
}
|
|
|
|
Or in the template for different error displays:
|
|
|
|
<% } else if (this.data.error_data) { %>
|
|
<% if (this.data.error_data.code === Ajax.ERROR_NOT_FOUND) { %>
|
|
<!-- Custom not-found UI -->
|
|
<div class="text-center">
|
|
<p>This project doesn't exist yet.</p>
|
|
<a href="..." class="btn btn-primary">Create It</a>
|
|
</div>
|
|
<% } else { %>
|
|
<!-- Standard error handling -->
|
|
<Universal_Error_Page_Component ... />
|
|
<% } %>
|
|
<% } else { %>
|
|
|
|
LOADING SPINNER
|
|
The Loading_Spinner component provides consistent loading UI:
|
|
|
|
<Loading_Spinner />
|
|
<Loading_Spinner $message="Loading project details..." />
|
|
|
|
Located at: rsx/theme/components/feedback/loading_spinner.jqhtml
|
|
|
|
COMPLETE EXAMPLE
|
|
From rsx/app/frontend/projects/Projects_View_Action.js:
|
|
|
|
@route('/projects/view/:id')
|
|
@layout('Frontend_Spa_Layout')
|
|
@spa('Frontend_Spa_Controller::index')
|
|
@title('Project Details')
|
|
class Projects_View_Action extends Spa_Action {
|
|
on_create() {
|
|
this.data.project = { name: '' };
|
|
this.data.error_data = null;
|
|
this.data.loading = true;
|
|
}
|
|
|
|
async on_load() {
|
|
try {
|
|
this.data.project = await Frontend_Projects_Controller
|
|
.get_project({ id: this.args.id });
|
|
} catch (e) {
|
|
this.data.error_data = e;
|
|
}
|
|
this.data.loading = false;
|
|
}
|
|
}
|
|
|
|
The template uses the three-state pattern with full page content
|
|
in the success state. See the actual file for complete template.
|
|
|
|
WHEN TO USE THIS PATTERN
|
|
Use for:
|
|
- Detail/view pages loading a single record by ID
|
|
- Edit pages that need to load existing data
|
|
- Any page where initial data might not exist or be accessible
|
|
|
|
Not needed for:
|
|
- List pages (DataGrid handles its own loading/error states)
|
|
- Create pages (no existing data to load)
|
|
- Static pages without dynamic data
|
|
|
|
SEE ALSO
|
|
spa(3), ajax_error_handling(3), jqhtml(3)
|
|
|
|
RSX Framework 2025-11-21 VIEW_ACTION_PATTERNS(3)
|