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>
This commit is contained in:
@@ -20,7 +20,7 @@ SYNOPSIS
|
||||
|
||||
on_ready() {
|
||||
// All children ready, safe for DOM manipulation
|
||||
this.$id('edit').on('click', () => this.edit());
|
||||
this.$sid('edit').on('click', () => this.edit());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,10 +83,10 @@ TEMPLATE SYNTAX
|
||||
|
||||
<Define:User_Card>
|
||||
<div class="card">
|
||||
<img $id="avatar" src="<%= this.data.avatar %>" />
|
||||
<h3 $id="name"><%= this.data.name %></h3>
|
||||
<p $id="email"><%= this.data.email %></p>
|
||||
<button $id="edit">Edit</button>
|
||||
<img $sid="avatar" src="<%= this.data.avatar %>" />
|
||||
<h3 $sid="name"><%= this.data.name %></h3>
|
||||
<p $sid="email"><%= this.data.email %></p>
|
||||
<button $sid="edit">Edit</button>
|
||||
</div>
|
||||
</Define:User_Card>
|
||||
|
||||
@@ -94,9 +94,10 @@ TEMPLATE SYNTAX
|
||||
<%= expression %> - Escaped HTML output (safe, default)
|
||||
<%!= expression %> - Unescaped raw output (pre-sanitized content only)
|
||||
<% statement; %> - JavaScript statements (loops, conditionals)
|
||||
<%-- comment --%> - JQHTML comments (not HTML <!-- --> comments)
|
||||
|
||||
Attributes:
|
||||
$id="name" - Scoped ID (becomes id="name:component_id")
|
||||
$sid="name" - Scoped ID (becomes id="name:component_id")
|
||||
$attr=value - Component parameter (becomes this.args.attr)
|
||||
Note: Also creates data-attr HTML attribute
|
||||
@event=this.method - Event binding (⚠️ verify functionality)
|
||||
@@ -255,7 +256,7 @@ THIS.ARGS VS THIS.DATA
|
||||
Lifecycle Restrictions (ENFORCED):
|
||||
- on_create(): Can modify this.data (set defaults)
|
||||
- on_load(): Can ONLY access this.args and this.data
|
||||
Cannot access this.$, this.$id(), or any other properties
|
||||
Cannot access this.$, this.$sid(), or any other properties
|
||||
Can modify this.data freely
|
||||
- on_ready() / event handlers: Can modify this.args, read this.data
|
||||
CANNOT modify this.data (frozen)
|
||||
@@ -286,7 +287,7 @@ THIS.ARGS VS THIS.DATA
|
||||
|
||||
on_ready() {
|
||||
// Modify state, then reload
|
||||
this.$id('filter_btn').on('click', () => {
|
||||
this.$sid('filter_btn').on('click', () => {
|
||||
this.args.filter = 'active'; // Change state
|
||||
this.reload(); // Re-fetch with new state
|
||||
});
|
||||
@@ -375,6 +376,50 @@ CONTROL FLOW AND LOOPS
|
||||
%>
|
||||
<p>Total: $<%= total.toFixed(2) %></p>
|
||||
|
||||
COMMENTS IN TEMPLATES
|
||||
JQHTML uses its own comment syntax, NOT HTML comments:
|
||||
|
||||
Correct - JQHTML comments (parser removes, never in output):
|
||||
<%--
|
||||
This is a JQHTML comment
|
||||
Completely removed during compilation
|
||||
Perfect for component documentation
|
||||
--%>
|
||||
|
||||
Incorrect - HTML comments (parser DOES NOT remove):
|
||||
<!--
|
||||
This is an HTML comment
|
||||
Parser treats this as literal HTML
|
||||
Will appear in rendered output
|
||||
Still processes JQHTML directives inside!
|
||||
-->
|
||||
|
||||
Critical difference:
|
||||
HTML comments <!-- --> do NOT block JQHTML directive execution.
|
||||
Code inside HTML comments will still execute, just like PHP code
|
||||
inside HTML comments in .php files still executes.
|
||||
|
||||
WRONG - This WILL execute:
|
||||
<!-- <% dangerous_code(); %> -->
|
||||
|
||||
CORRECT - This will NOT execute:
|
||||
<%-- <% safe_code(); %> --%>
|
||||
|
||||
Component docblocks:
|
||||
Use JQHTML comments at the top of component templates:
|
||||
|
||||
<%--
|
||||
User_Card_Component
|
||||
|
||||
Displays user profile information in a card layout.
|
||||
|
||||
$user_id - ID of user to display
|
||||
$show_avatar - Whether to show profile photo (default: true)
|
||||
--%>
|
||||
<Define:User_Card_Component>
|
||||
<!-- Component template here -->
|
||||
</Define:User_Card_Component>
|
||||
|
||||
COMPONENT LIFECYCLE
|
||||
Five-stage deterministic lifecycle:
|
||||
|
||||
@@ -403,7 +448,7 @@ COMPONENT LIFECYCLE
|
||||
4. on_load() (bottom-up, siblings in parallel, CAN be async)
|
||||
- Load async data based on this.args
|
||||
- ONLY access this.args and this.data (RESTRICTED)
|
||||
- CANNOT access this.$, this.$id(), or any other properties
|
||||
- CANNOT access this.$, this.$sid(), or any other properties
|
||||
- ONLY modify this.data - NEVER DOM
|
||||
- NO child component access
|
||||
- Siblings at same depth execute in parallel
|
||||
@@ -642,7 +687,7 @@ JAVASCRIPT COMPONENT CLASS
|
||||
on_ready() {
|
||||
// All children ready, safe for DOM
|
||||
// Attach event handlers
|
||||
this.$id('select_all').on('click', () => this.select_all());
|
||||
this.$sid('select_all').on('click', () => this.select_all());
|
||||
this.$.animate({opacity: 1}, 300);
|
||||
}
|
||||
|
||||
@@ -846,12 +891,12 @@ $REDRAWABLE ATTRIBUTE - LIGHTWEIGHT COMPONENTS
|
||||
converts the element into a <Redrawable> component:
|
||||
|
||||
<!-- Write this: -->
|
||||
<div $redrawable $id="counter">
|
||||
<div $redrawable $sid="counter">
|
||||
Count: <%= this.data.count %>
|
||||
</div>
|
||||
|
||||
<!-- Parser transforms to: -->
|
||||
<Redrawable data-tag="div" $id="counter">
|
||||
<Redrawable data-tag="div" $sid="counter">
|
||||
Count: <%= this.data.count %>
|
||||
</Redrawable>
|
||||
|
||||
@@ -867,12 +912,12 @@ $REDRAWABLE ATTRIBUTE - LIGHTWEIGHT COMPONENTS
|
||||
async increment_counter() {
|
||||
this.data.count++;
|
||||
// Re-render only the counter element, not entire dashboard
|
||||
this.render('counter'); // Finds child with $id="counter"
|
||||
this.render('counter'); // Finds child with $sid="counter"
|
||||
}
|
||||
}
|
||||
|
||||
render(id) Delegation Syntax:
|
||||
- this.render('counter') finds child with $id="counter"
|
||||
- this.render('counter') finds child with $sid="counter"
|
||||
- Verifies element is a component (has $redrawable or is proper
|
||||
component class)
|
||||
- Calls its render() method to update only that element
|
||||
@@ -881,7 +926,7 @@ $REDRAWABLE ATTRIBUTE - LIGHTWEIGHT COMPONENTS
|
||||
- Parent component's DOM remains unchanged
|
||||
|
||||
Error Handling:
|
||||
- Clear error if $id doesn't exist in children
|
||||
- Clear error if $sid doesn't exist in children
|
||||
- Clear error if element isn't configured as component
|
||||
- Guides developers to correct usage patterns
|
||||
|
||||
@@ -922,7 +967,7 @@ LIFECYCLE MANIPULATION METHODS
|
||||
}
|
||||
|
||||
on_ready() {
|
||||
this.$id('filter_btn').on('click', async () => {
|
||||
this.$sid('filter_btn').on('click', async () => {
|
||||
this.args.filter = 'active'; // Update state
|
||||
await this.reload(); // Re-fetch with new state
|
||||
});
|
||||
@@ -1047,26 +1092,26 @@ DOM UTILITIES
|
||||
jQuery wrapped component root element
|
||||
This is genuine jQuery - all methods work directly
|
||||
|
||||
this.$id(name)
|
||||
this.$sid(name)
|
||||
Get scoped element as jQuery object
|
||||
Example: this.$id('edit') gets element with $id="edit"
|
||||
Example: this.$sid('edit') gets element with $sid="edit"
|
||||
Returns jQuery object, NOT component instance
|
||||
|
||||
this.id(name)
|
||||
this.sid(name)
|
||||
Get scoped child component instance directly
|
||||
Example: this.id('my_component') gets component instance
|
||||
Example: this.sid('my_component') gets component instance
|
||||
Returns component instance, NOT jQuery object
|
||||
|
||||
CRITICAL: this.$id() vs this.id() distinction
|
||||
- this.$id('foo') → jQuery object (for DOM manipulation)
|
||||
- this.id('foo') → Component instance (for calling methods)
|
||||
CRITICAL: this.$sid() vs this.sid() distinction
|
||||
- this.$sid('foo') → jQuery object (for DOM manipulation)
|
||||
- this.sid('foo') → Component instance (for calling methods)
|
||||
|
||||
Common mistake:
|
||||
const comp = this.id('foo').component(); // ❌ WRONG
|
||||
const comp = this.id('foo'); // ✅ CORRECT
|
||||
const comp = this.sid('foo').component(); // ❌ WRONG
|
||||
const comp = this.sid('foo'); // ✅ CORRECT
|
||||
|
||||
Getting component from jQuery:
|
||||
const $elem = this.$id('foo');
|
||||
const $elem = this.$sid('foo');
|
||||
const comp = $elem.component(); // ✅ CORRECT (jQuery → component)
|
||||
|
||||
this.data
|
||||
@@ -1105,13 +1150,13 @@ NESTING COMPONENTS
|
||||
on_load, on_ready).
|
||||
|
||||
SCOPED IDS
|
||||
Use $id attribute for component-scoped element IDs:
|
||||
Use $sid attribute for component-scoped element IDs:
|
||||
|
||||
Template:
|
||||
<Define:User_Card>
|
||||
<h3 $id="title">Name</h3>
|
||||
<p $id="email">Email</p>
|
||||
<button $id="edit_btn">Edit</button>
|
||||
<h3 $sid="title">Name</h3>
|
||||
<p $sid="email">Email</p>
|
||||
<button $sid="edit_btn">Edit</button>
|
||||
</Define:User_Card>
|
||||
|
||||
Rendered HTML (automatic scoping):
|
||||
@@ -1121,13 +1166,13 @@ SCOPED IDS
|
||||
<button id="edit_btn:c123">Edit</button>
|
||||
</div>
|
||||
|
||||
Access with this.$id():
|
||||
Access with this.$sid():
|
||||
class User_Card extends Jqhtml_Component {
|
||||
on_ready() {
|
||||
// Use logical name
|
||||
this.$id('title').text('John Doe');
|
||||
this.$id('email').text('john@example.com');
|
||||
this.$id('edit_btn').on('click', () => this.edit());
|
||||
this.$sid('title').text('John Doe');
|
||||
this.$sid('email').text('john@example.com');
|
||||
this.$sid('edit_btn').on('click', () => this.edit());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1148,12 +1193,12 @@ EXAMPLES
|
||||
|
||||
on_ready() {
|
||||
// Attach event handlers after data loaded
|
||||
this.$id('buy').on('click', async () => {
|
||||
this.$sid('buy').on('click', async () => {
|
||||
await Cart.add(this.data.id);
|
||||
this.$id('buy').text('Added!').prop('disabled', true);
|
||||
this.$sid('buy').text('Added!').prop('disabled', true);
|
||||
});
|
||||
|
||||
this.$id('favorite').on('click', () => {
|
||||
this.$sid('favorite').on('click', () => {
|
||||
this.$.toggleClass('favorited');
|
||||
});
|
||||
}
|
||||
@@ -1208,19 +1253,19 @@ EXAMPLES
|
||||
}
|
||||
|
||||
validate() {
|
||||
const email = this.$id('email').val();
|
||||
const email = this.$sid('email').val();
|
||||
if (!email.includes('@')) {
|
||||
this.$id('error').text('Invalid email');
|
||||
this.$sid('error').text('Invalid email');
|
||||
return false;
|
||||
}
|
||||
this.$id('error').text('');
|
||||
this.$sid('error').text('');
|
||||
return true;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
const data = {
|
||||
email: this.$id('email').val(),
|
||||
message: this.$id('message').val(),
|
||||
email: this.$sid('email').val(),
|
||||
message: this.$sid('message').val(),
|
||||
};
|
||||
|
||||
await fetch('/contact', {
|
||||
@@ -1391,6 +1436,70 @@ CONTENT AND SLOTS
|
||||
- Form patterns with customizable field sets
|
||||
- Any component hierarchy with shared structure
|
||||
|
||||
TEMPLATE-ONLY COMPONENTS
|
||||
Components can exist as .jqhtml files without a companion .js file.
|
||||
This is fine for simple display-only components that just render
|
||||
input data with conditionals - no lifecycle hooks or event handlers
|
||||
needed beyond what's possible inline.
|
||||
|
||||
When to use template-only:
|
||||
- Component just displays data passed via arguments
|
||||
- Only needs simple conditionals in the template
|
||||
- No complex event handling beyond simple button clicks
|
||||
- Mentally easier than creating a separate .js file
|
||||
|
||||
Inline Event Handlers:
|
||||
Define handlers in template code, reference with @event syntax:
|
||||
|
||||
<Define:Retry_Button>
|
||||
<% this.handle_click = () => window.location.reload(); %>
|
||||
<button class="btn btn-primary" @click=this.handle_click>
|
||||
Retry
|
||||
</button>
|
||||
</Define:Retry_Button>
|
||||
|
||||
Note: @event values must be UNQUOTED (not @click="this.method").
|
||||
|
||||
Inline Argument Validation:
|
||||
Throw errors early if required arguments are missing:
|
||||
|
||||
<Define:User_Badge>
|
||||
<%
|
||||
if (!this.args.user_id) {
|
||||
throw new Error('User_Badge: $user_id is required');
|
||||
}
|
||||
if (!this.args.name) {
|
||||
throw new Error('User_Badge: $name is required');
|
||||
}
|
||||
%>
|
||||
<span class="badge"><%= this.args.name %></span>
|
||||
</Define:User_Badge>
|
||||
|
||||
Complete Example (error page component):
|
||||
<%--
|
||||
Not_Found_Error_Page_Component
|
||||
Displays when a record cannot be found.
|
||||
--%>
|
||||
<Define:Not_Found_Error_Page_Component class="text-center py-5">
|
||||
<div class="mb-4">
|
||||
<i class="bi bi-exclamation-triangle-fill text-warning"
|
||||
style="font-size: 4rem;"></i>
|
||||
</div>
|
||||
<h3 class="mb-3"><%= this.args.record_type %> Not Found</h3>
|
||||
<p class="text-muted mb-4">
|
||||
The <%= this.args.record_type.toLowerCase() %> you are
|
||||
looking for does not exist or has been deleted.
|
||||
</p>
|
||||
<a href="<%= this.args.back_url %>" class="btn btn-primary">
|
||||
<%= this.args.back_label %>
|
||||
</a>
|
||||
</Define:Not_Found_Error_Page_Component>
|
||||
|
||||
This pattern is not necessarily "best practice" for complex components,
|
||||
but it works well and is pragmatic for simple display components. If
|
||||
the component needs lifecycle hooks, state management, or complex
|
||||
event handling, create a companion .js file instead.
|
||||
|
||||
INTEGRATION WITH RSX
|
||||
JQHTML automatically integrates with the RSX framework:
|
||||
- Templates discovered by manifest system during build
|
||||
|
||||
230
app/RSpade/man/view_action_patterns.txt
Executable file
230
app/RSpade/man/view_action_patterns.txt
Executable file
@@ -0,0 +1,230 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user