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:
root
2025-11-21 04:35:01 +00:00
parent 081fc0b88e
commit 78553d4edf
899 changed files with 8887 additions and 7868 deletions

View File

@@ -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

View 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)