Files
rspade_system/app/RSpade/man/view_action_patterns.txt
root 78553d4edf Fix code quality violations for publish
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>
2025-11-21 04:35:01 +00:00

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)