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

@@ -180,11 +180,11 @@ class Component_Create_Command extends Command
$display_name = str_replace('_Component', '', $class_name);
return <<<JQHTML
<!--
<%--
{$class_name}
\$button_text="Click Me" - Text shown on the button
-->
--%>
<Define:{$class_name} style="border: 1px solid black;">
<h4>{$display_name}</h4>
<div \$id="hello_world" style="font-weight: bold; display: none;">

View File

@@ -29,11 +29,42 @@ use App\RSpade\Core\Response\Rsx_Response_Abstract;
*/
class Ajax
{
// Error code constants
const ERROR_VALIDATION = 'validation';
const ERROR_NOT_FOUND = 'not_found';
const ERROR_UNAUTHORIZED = 'unauthorized';
const ERROR_AUTH_REQUIRED = 'auth_required';
const ERROR_FATAL = 'fatal';
const ERROR_GENERIC = 'generic';
const ERROR_SERVER = 'server_error'; // Client-generated (HTTP 500)
const ERROR_NETWORK = 'network_error'; // Client-generated (connection failed)
/**
* Flag to indicate AJAX response mode for error handlers
*/
protected static bool $ajax_response_mode = false;
/**
* Get default message for error code
*
* @param string $error_code One of the ERROR_* constants
* @return string Default user-friendly message
*/
public static function get_default_message(string $error_code): string
{
return match ($error_code) {
self::ERROR_VALIDATION => 'Please correct the errors below',
self::ERROR_NOT_FOUND => 'The requested record was not found',
self::ERROR_UNAUTHORIZED => 'You do not have permission to perform this action',
self::ERROR_AUTH_REQUIRED => 'Please log in to continue',
self::ERROR_FATAL => 'A fatal error has occurred',
self::ERROR_SERVER => 'A server error occurred. Please try again.',
self::ERROR_NETWORK => 'Could not connect to server. Please check your connection.',
self::ERROR_GENERIC => 'An error has occurred',
default => 'An error has occurred',
};
}
/**
* Call an internal API method directly from PHP code
*
@@ -157,19 +188,26 @@ class Ajax
$details = $response->get_details();
switch ($type) {
case 'response_auth_required':
case self::ERROR_AUTH_REQUIRED:
throw new AjaxAuthRequiredException($reason);
case 'response_unauthorized':
case self::ERROR_UNAUTHORIZED:
throw new AjaxUnauthorizedException($reason);
case 'response_form_error':
case self::ERROR_VALIDATION:
case self::ERROR_NOT_FOUND:
throw new AjaxFormErrorException($reason, $details);
case 'fatal':
case self::ERROR_FATAL:
$message = $reason;
if (!empty($details)) {
$message .= ' - ' . json_encode($details);
}
throw new AjaxFatalErrorException($message);
case self::ERROR_GENERIC:
throw new Exception($reason);
default:
throw new Exception("Unknown RSX response type: {$type}");
}
@@ -324,18 +362,14 @@ class Ajax
throw new Exception($message);
}
// Build error response based on type
// Build error response
$json_response = [
'_success' => false,
'error_type' => $type,
'error_code' => $response->get_error_code(),
'reason' => $response->get_reason(),
'metadata' => $response->get_metadata(),
];
// Add details for form errors
if ($type === 'response_form_error') {
$json_response['details'] = $response->get_details();
}
// Add console debug messages if any
$console_messages = \App\RSpade\Core\Debug\Debugger::_get_console_messages();
if (!empty($console_messages)) {
@@ -395,7 +429,7 @@ class Ajax
if ((array_key_exists('_success', $response) && is_bool($response['_success'])) ||
(array_key_exists('success', $response) && is_bool($response['success']))) {
$wrong_way = "return ['_success' => false, 'message' => 'Error'];";
$right_way_validation = "return response_form_error('Validation failed', ['email' => 'Invalid']);";
$right_way_validation = "return response_error(Ajax::ERROR_VALIDATION, ['email' => 'Invalid email']);";
$right_way_success = "return ['user_id' => 123, 'data' => [...]];\n// Framework wraps: {_success: true, _ajax_return_value: {...}}";
$right_way_exception = "// Let exceptions bubble - framework handles them\n\$user->save(); // Don't wrap in try/catch";

View File

@@ -101,30 +101,33 @@ class Ajax_Batch_Controller extends Rsx_Controller_Abstract
} catch (Exceptions\AjaxAuthRequiredException $e) {
$responses["C_{$call_id}"] = [
'success' => false,
'error_type' => 'response_auth_required',
'error_code' => Ajax::ERROR_AUTH_REQUIRED,
'reason' => $e->getMessage(),
'metadata' => [],
];
} catch (Exceptions\AjaxUnauthorizedException $e) {
$responses["C_{$call_id}"] = [
'success' => false,
'error_type' => 'response_unauthorized',
'error_code' => Ajax::ERROR_UNAUTHORIZED,
'reason' => $e->getMessage(),
'metadata' => [],
];
} catch (Exceptions\AjaxFormErrorException $e) {
$responses["C_{$call_id}"] = [
'success' => false,
'error_type' => 'response_form_error',
'error_code' => Ajax::ERROR_VALIDATION,
'reason' => $e->getMessage(),
'details' => $e->get_details(),
'metadata' => $e->get_details(),
];
} catch (Exceptions\AjaxFatalErrorException $e) {
$responses["C_{$call_id}"] = [
'success' => false,
'error_type' => 'fatal',
'error_code' => Ajax::ERROR_FATAL,
'reason' => $e->getMessage(),
'metadata' => [],
];
} catch (\Exception $e) {

View File

@@ -2176,16 +2176,20 @@ JS;
foreach ($file_info['public_static_methods'] ?? [] as $method_name => $method_data) {
foreach ($method_data['attributes'] ?? [] as $attr_name => $attr_instances) {
if ($attr_name === 'Route') {
// Get the first route pattern (index 0 is the first argument)
// Collect all route patterns for this method (supports multiple #[Route] attributes)
foreach ($attr_instances as $instance) {
$route_pattern = $instance[0] ?? null;
if ($route_pattern) {
console_debug('BUNDLE', " Found route: {$class_name}::{$method_name} => {$route_pattern}");
// Store route info
// Initialize arrays if needed
if (!isset($routes[$class_name])) {
$routes[$class_name] = [];
}
$routes[$class_name][$method_name] = $route_pattern;
if (!isset($routes[$class_name][$method_name])) {
$routes[$class_name][$method_name] = [];
}
// Append route pattern to array
$routes[$class_name][$method_name][] = $route_pattern;
}
}
}
@@ -2231,15 +2235,19 @@ JS;
foreach ($file_info['public_static_methods'] ?? [] as $method_name => $method_data) {
foreach ($method_data['attributes'] ?? [] as $attr_name => $attr_instances) {
if ($attr_name === 'Route') {
// Get the first route pattern (index 0 is the first argument)
// Collect all route patterns for this method (supports multiple #[Route] attributes)
foreach ($attr_instances as $instance) {
$route_pattern = $instance[0] ?? null;
if ($route_pattern) {
// Store route info
// Initialize arrays if needed
if (!isset($routes[$class_name])) {
$routes[$class_name] = [];
}
$routes[$class_name][$method_name] = $route_pattern;
if (!isset($routes[$class_name][$method_name])) {
$routes[$class_name][$method_name] = [];
}
// Append route pattern to array
$routes[$class_name][$method_name][] = $route_pattern;
}
}
}

View File

@@ -26,7 +26,7 @@ class Rsx_Reference_Data_Controller extends Rsx_Controller_Abstract
* Returns array of {value: country_code, label: country_name} sorted alphabetically
*/
#[Ajax_Endpoint]
public static function countries(Request $request, array $params = []): array
public static function countries(Request $request, array $params = [])
{
return Country_Model::enabled()
->orderBy('name')
@@ -41,10 +41,9 @@ class Rsx_Reference_Data_Controller extends Rsx_Controller_Abstract
*
* @param Request $request
* @param array $params - Expected: ['country' => 'US']
* @return array
*/
#[Ajax_Endpoint]
public static function states(Request $request, array $params = []): array
public static function states(Request $request, array $params = [])
{
$country = $params['country'] ?? 'US';

View File

@@ -19,10 +19,9 @@ class Debugger_Controller extends Rsx_Controller_Abstract
*
* @param \Illuminate\Http\Request $request
* @param array $params
* @return array
*/
#[Ajax_Endpoint]
public static function log_console_messages(Request $request, array $params = []): array
public static function log_console_messages(Request $request, array $params = [])
{
return Debugger::log_console_messages($request, $params);
}
@@ -32,10 +31,9 @@ class Debugger_Controller extends Rsx_Controller_Abstract
*
* @param \Illuminate\Http\Request $request
* @param array $params
* @return array
*/
#[Ajax_Endpoint]
public static function log_browser_errors(Request $request, array $params = []): array
public static function log_browser_errors(Request $request, array $params = [])
{
return Debugger::log_browser_errors($request, $params);
}

View File

@@ -782,7 +782,7 @@ class Dispatcher
}
// Handle authentication required
if ($type === 'response_auth_required') {
if ($type === \App\RSpade\Core\Ajax\Ajax::ERROR_AUTH_REQUIRED) {
if ($redirect_url) {
Rsx::flash_error($reason);
@@ -793,7 +793,7 @@ class Dispatcher
}
// Handle unauthorized
if ($type === 'response_unauthorized') {
if ($type === \App\RSpade\Core\Ajax\Ajax::ERROR_UNAUTHORIZED) {
if ($redirect_url) {
Rsx::flash_error($reason);
@@ -803,8 +803,8 @@ class Dispatcher
throw new Exception($reason);
}
// Handle form error
if ($type === 'response_form_error') {
// Handle validation and not found errors
if ($type === \App\RSpade\Core\Ajax\Ajax::ERROR_VALIDATION || $type === \App\RSpade\Core\Ajax\Ajax::ERROR_NOT_FOUND) {
// Only redirect if this was a POST request
if (request()->isMethod('POST')) {
Rsx::flash_error($reason);

View File

@@ -7,6 +7,16 @@
* Batches up to 20 calls or flushes after setTimeout(0) debounce.
*/
class Ajax {
// Error code constants (must match server-side Ajax::ERROR_* constants)
static ERROR_VALIDATION = 'validation';
static ERROR_NOT_FOUND = 'not_found';
static ERROR_UNAUTHORIZED = 'unauthorized';
static ERROR_AUTH_REQUIRED = 'auth_required';
static ERROR_FATAL = 'fatal';
static ERROR_GENERIC = 'generic';
static ERROR_SERVER = 'server_error'; // Client-generated (HTTP 500)
static ERROR_NETWORK = 'network_error'; // Client-generated (connection failed)
/**
* Initialize Ajax system
* Called automatically when class is loaded
@@ -198,81 +208,70 @@ class Ajax {
resolve(processed_value);
} else {
// Handle error responses
const error_type = response.error_type || 'unknown_error';
const reason = response.reason || 'Unknown error occurred';
const details = response.details || {};
const error_code = response.error_code || Ajax.ERROR_GENERIC;
const reason = response.reason || 'An error occurred';
const metadata = response.metadata || {};
// Handle specific error types
switch (error_type) {
case 'fatal':
// Fatal PHP error with full error details
// Create error object
const error = new Error(reason);
error.code = error_code;
error.metadata = metadata;
// Handle fatal errors specially
if (error_code === Ajax.ERROR_FATAL) {
const fatal_error_data = response.error || {};
const error_message = fatal_error_data.error || 'Fatal error occurred';
error.message = fatal_error_data.error || 'Fatal error occurred';
error.metadata = response.error;
console.error('Ajax error response from server:', response.error);
const fatal_error = new Error(error_message);
fatal_error.type = 'fatal';
fatal_error.details = response.error;
// Log to server if browser error logging is enabled
// Log to server
Debugger.log_error({
message: `Ajax Fatal Error: ${error_message}`,
message: `Ajax Fatal Error: ${error.message}`,
type: 'ajax_fatal',
endpoint: url,
details: response.error,
});
reject(fatal_error);
break;
case 'response_auth_required':
console.error(
'The user is no longer authenticated, this is a placeholder for future code which handles this scenario.'
);
const auth_error = new Error(reason);
auth_error.type = 'auth_required';
auth_error.details = details;
reject(auth_error);
break;
case 'response_unauthorized':
console.error(
'The user is unauthorized to perform this action, this is a placeholder for future code which handles this scenario.'
);
const unauth_error = new Error(reason);
unauth_error.type = 'unauthorized';
unauth_error.details = details;
reject(unauth_error);
break;
case 'response_form_error':
const form_error = new Error(reason);
form_error.type = 'form_error';
form_error.details = details;
reject(form_error);
break;
default:
const generic_error = new Error(reason);
generic_error.type = error_type;
generic_error.details = details;
reject(generic_error);
break;
}
// Log auth errors for debugging
if (error_code === Ajax.ERROR_AUTH_REQUIRED) {
console.error('User is no longer authenticated');
}
if (error_code === Ajax.ERROR_UNAUTHORIZED) {
console.error('User is unauthorized to perform this action');
}
reject(error);
}
},
error: (xhr, status, error) => {
const error_message = Ajax._extract_error_message(xhr);
const network_error = new Error(error_message);
network_error.type = 'network_error';
network_error.status = xhr.status;
network_error.statusText = status;
const err = new Error();
// Log server errors (500+) to the server if browser error logging is enabled
// Determine error code based on status
if (xhr.status >= 500) {
// Server error (PHP crashed)
err.code = Ajax.ERROR_SERVER;
err.message = 'A server error occurred. Please try again.';
} else if (xhr.status === 0 || status === 'timeout' || status === 'error') {
// Network error (connection failed)
err.code = Ajax.ERROR_NETWORK;
err.message = 'Could not connect to server. Please check your connection.';
} else {
// Generic error
err.code = Ajax.ERROR_GENERIC;
err.message = Ajax._extract_error_message(xhr);
}
err.metadata = {
status: xhr.status,
statusText: status
};
// Log server errors to server
if (xhr.status >= 500) {
Debugger.log_error({
message: `Ajax Server Error ${xhr.status}: ${error_message}`,
message: `Ajax Server Error ${xhr.status}: ${err.message}`,
type: 'ajax_server_error',
endpoint: url,
status: xhr.status,
@@ -280,7 +279,7 @@ class Ajax {
});
}
reject(network_error);
reject(err);
},
});
});
@@ -376,26 +375,13 @@ class Ajax {
});
} else {
// Handle error
const error_type = call_response.error_type || 'unknown_error';
let error_message;
let error_details;
if (error_type === 'fatal' && call_response.error) {
// Fatal PHP error with full error details
const fatal_error_data = call_response.error;
error_message = fatal_error_data.error || 'Fatal error occurred';
error_details = call_response.error;
console.error('Ajax error response from server:', call_response.error);
} else {
// Other error types
error_message = call_response.reason || 'Unknown error occurred';
error_details = call_response.details || {};
}
const error_code = call_response.error_code || Ajax.ERROR_GENERIC;
const error_message = call_response.reason || 'Unknown error occurred';
const metadata = call_response.metadata || {};
const error = new Error(error_message);
error.type = error_type;
error.details = error_details;
error.code = error_code;
error.metadata = metadata;
pending_call.is_error = true;
pending_call.error = error;
@@ -410,7 +396,8 @@ class Ajax {
// Network or server error - reject all pending calls
const error_message = Ajax._extract_error_message(xhr_error);
const error = new Error(error_message);
error.type = 'network_error';
error.code = Ajax.ERROR_NETWORK;
error.metadata = {};
for (const call_id in call_map) {
const pending_call = call_map[call_id];

View File

@@ -30,8 +30,39 @@ class Debugger {
// Check if browser error logging is enabled
if (window.rsxapp && window.rsxapp.log_browser_errors) {
// Listen for unhandled exceptions from Rsx event system
Rsx.on('unhandled_exception', function (error_data) {
Debugger._handle_browser_error(error_data);
Rsx.on('unhandled_exception', function (payload) {
// Extract exception from payload
const exception = payload.exception;
// Normalize exception to error data object
// Contract: exception can be Error object or string
let errorData = {};
if (exception instanceof Error) {
// Extract properties from Error object
errorData.message = exception.message;
errorData.stack = exception.stack;
errorData.filename = exception.filename;
errorData.lineno = exception.lineno;
errorData.colno = exception.colno;
errorData.type = 'exception';
} else if (typeof exception === 'string') {
// Plain string message
errorData.message = exception;
errorData.type = 'manual';
} else if (exception && typeof exception === 'object') {
// Object with message property (structured error)
errorData = exception;
if (!errorData.type) {
errorData.type = 'manual';
}
} else {
// Default case for unknown types
errorData.message = String(exception);
errorData.type = 'unknown';
}
Debugger._handle_browser_error(errorData);
});
}

View File

@@ -0,0 +1,193 @@
/**
* Exception_Handler
*
* Centralized exception display logic for unhandled exceptions.
* Decides whether to show exception in SPA layout (debug mode) or as flash alert.
*
* Architecture:
* - window error/unhandledrejection → Rsx._handle_unhandled_exception()
* - Rsx triggers 'unhandled_exception' event (for logging via Debugger.js)
* - Rsx disables SPA navigation
* - Rsx calls Exception_Handler.display_unhandled_exception()
* - Exception_Handler checks conditions and shows either:
* a) Debug error box in layout (if SPA, debug mode, action loading)
* b) Flash alert (when layout unavailable)
*
* Display Conditions for Layout Debug UI:
* 1. Must be in SPA mode (window.rsxapp.is_spa)
* 2. Must be in debug mode (window.rsxapp.debug)
* 3. Spa.layout must exist with show_debug_exception() method
* 4. Spa.layout.action must exist
* 5. Action must still be loading (Spa.layout._action_is_loading === true)
* 6. Developer has not suppressed display (suppress_display() not called)
*
* If conditions fail, falls back to flash alert.
* If layout display throws, falls back to flash alert.
*/
class Exception_Handler {
/**
* Developer-controlled flag to suppress all exception display UI
* @type {boolean}
*/
static _suppress_display = false;
/**
* Timestamp of last flash alert for rate limiting
* @type {number}
*/
static _last_exception_flash = 0;
/**
* Register exception handler during framework initialization
* Called automatically by framework - do not call manually
*/
static _on_framework_core_init() {
// Listen for all unhandled exceptions to display them
Rsx.on('unhandled_exception', function(payload) {
// Extract exception and metadata from payload
const exception = payload.exception;
const meta = payload.meta || {};
Exception_Handler.display_unhandled_exception(exception, meta);
});
}
/**
* Suppress all exception display UI
* Use this when you want to handle exceptions completely custom
*/
static suppress_display() {
Exception_Handler._suppress_display = true;
}
/**
* Resume exception display UI after suppressing
*/
static resume_display() {
Exception_Handler._suppress_display = false;
}
/**
* Display an unhandled exception using the most appropriate method
*
* Decision tree:
* 1. If developer suppressed display → do nothing
* 2. Log to console if from global handler (not already logged by catcher)
* 3. If in SPA, debug mode, layout exists, action not ready → show in layout
* 4. Otherwise → show flash alert
*
* @param {Error|string} exception - The exception to display
* @param {Object} meta - Metadata about exception source
* @param {string} meta.source - 'window_error', 'unhandled_rejection', or undefined
*/
static display_unhandled_exception(exception, meta = {}) {
// Developer explicitly suppressed all display
if (Exception_Handler._suppress_display) {
return;
}
// Log to console if from global handler (true unhandled error)
// Skip if manually triggered - already logged by the catcher
if (meta.source === 'window_error' || meta.source === 'unhandled_rejection') {
console.error('[Exception_Handler] Unhandled exception:', exception);
}
// Try to show in layout if all conditions met
if (Exception_Handler._should_show_in_layout()) {
try {
Spa.layout.show_debug_exception(exception);
return; // Successfully shown in layout, don't show flash
} catch (e) {
// Failed to show in layout (maybe $content doesn't exist)
// Fall through to flash alert
console.warn('[Exception_Handler] Failed to show exception in layout:', e);
}
}
// Show as flash alert when layout display unavailable
Exception_Handler._show_flash_alert(exception);
}
/**
* Check if we should attempt to show exception in SPA layout
*
* @returns {boolean}
*/
static _should_show_in_layout() {
// Must be in SPA mode
if (!window.rsxapp || !window.rsxapp.is_spa) {
return false;
}
// Must be in debug mode
if (!window.rsxapp.debug) {
return false;
}
// Spa must be loaded
if (typeof Spa === 'undefined') {
return false;
}
// Layout must exist and have the display method
if (!Spa.layout || typeof Spa.layout.show_debug_exception !== 'function') {
return false;
}
// Action must exist
if (!Spa.layout.action) {
return false;
}
// Check if action is still loading
// During action load, Spa_Layout sets _action_is_loading flag
if (!Spa.layout._action_is_loading) {
return false;
}
return true;
}
/**
* Show exception as flash alert (rate limited)
*
* @param {Error|string} exception
*/
static _show_flash_alert(exception) {
// Flash_Alert must be available
if (typeof Flash_Alert === 'undefined') {
return;
}
// Extract message from Error object or string
let error_text;
if (exception instanceof Error) {
error_text = exception.message;
} else if (typeof exception === 'string') {
error_text = exception;
} else if (exception && typeof exception === 'object' && exception.message) {
error_text = exception.message;
} else {
error_text = 'Unknown error';
}
// Rate limit: max 1 flash alert per second
const now = Date.now();
if (now - Exception_Handler._last_exception_flash >= 1000) {
Exception_Handler._last_exception_flash = now;
// Truncate long messages in debug mode, show generic message in production
let message;
if (window.rsxapp && window.rsxapp.debug) {
message = error_text.length > 300
? error_text.substring(0, 300) + '...'
: error_text;
} else {
message = 'An unhandled error has occurred, you may need to refresh your page';
}
Flash_Alert.error(message);
}
}
}

View File

@@ -50,9 +50,6 @@ class Rsx {
// Gets set to true to interupt startup sequence
static __stopped = false;
// Timestamp of last flash alert for unhandled exceptions (rate limiting)
static _last_exception_flash = 0;
// Initialize event handlers storage
static _init_events() {
if (typeof Rsx._event_handlers === 'undefined') {
@@ -113,70 +110,101 @@ class Rsx {
/**
* Setup global unhandled exception handlers
* Must be called before framework initialization begins
*
* Exception Event Contract:
* -------------------------
* When exceptions occur, they are broadcast via:
* Rsx.trigger('unhandled_exception', { exception, meta })
*
* Event payload structure:
* {
* exception: Error|string, // The exception (Error object or string)
* meta: {
* source: 'window_error'|'unhandled_rejection'|undefined
* // source = undefined means manually triggered (already logged by catcher)
* // source = 'window_error' or 'unhandled_rejection' means needs logging
* }
* }
*
* The exception can be:
* 1. An Error object (preferred) - Has .message, .stack, .filename, .lineno properties
* 2. A string - Plain error message text
*
* Consumers should handle both formats:
* ```javascript
* Rsx.on('unhandled_exception', function(payload) {
* const exception = payload.exception;
* const meta = payload.meta || {};
*
* let message;
* if (exception instanceof Error) {
* message = exception.message;
* // Can also access: exception.stack, exception.filename, exception.lineno
* } else if (typeof exception === 'string') {
* message = exception;
* } else {
* message = String(exception);
* }
*
* // Only log if from global handler (not already logged)
* if (meta.source === 'window_error' || meta.source === 'unhandled_rejection') {
* console.error('[Handler]', exception);
* }
* });
* ```
*
* Exception Flow:
* - window error/unhandledrejection → _handle_unhandled_exception(exception, {source})
* - Triggers 'unhandled_exception' event with exception and metadata
* - Debugger.js: Logs to server
* - Exception_Handler: Logs to console (if from global handler) and displays
* - Disables SPA navigation
*/
static _setup_exception_handlers() {
// Handle uncaught JavaScript errors
window.addEventListener('error', function (event) {
Rsx._handle_unhandled_exception({
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error ? event.error.stack : null,
type: 'error',
error: event.error,
});
// Pass the Error object directly if available, otherwise create one
const exception = event.error || new Error(event.message);
// Attach additional metadata if not already present
if (!exception.filename) exception.filename = event.filename;
if (!exception.lineno) exception.lineno = event.lineno;
if (!exception.colno) exception.colno = event.colno;
Rsx._handle_unhandled_exception(exception, { source: 'window_error' });
});
// Handle unhandled promise rejections
window.addEventListener('unhandledrejection', function (event) {
Rsx._handle_unhandled_exception({
message: event.reason ? event.reason.message || String(event.reason) : 'Unhandled promise rejection',
stack: event.reason && event.reason.stack ? event.reason.stack : null,
type: 'unhandledrejection',
error: event.reason,
});
// event.reason can be Error, string, or any value
const exception = event.reason instanceof Error
? event.reason
: new Error(event.reason ? String(event.reason) : 'Unhandled promise rejection');
Rsx._handle_unhandled_exception(exception, { source: 'unhandled_rejection' });
});
}
/**
* Internal handler for unhandled exceptions
* Triggers event, shows flash alert (rate limited), disables SPA, and logs to console
* Triggers event and disables SPA
* Display and logging handled by Exception_Handler listening to the event
*
* @param {Error|string|Object} exception - Exception object, string, or object with message
* @param {Object} meta - Metadata about exception source
* @param {string} meta.source - 'window_error', 'unhandled_rejection', or undefined for manual triggers
*/
static _handle_unhandled_exception(error_data) {
// Always log to console
console.error('[Rsx] Unhandled exception:', error_data);
// Trigger event for listeners (e.g., Debugger for server logging)
Rsx.trigger('unhandled_exception', error_data);
static _handle_unhandled_exception(exception, meta = {}) {
// Trigger event for listeners:
// - Debugger.js: Logs to server
// - Exception_Handler: Logs to console and displays error (layout or flash alert)
// Pass exception and metadata (source info for logging decisions)
Rsx.trigger('unhandled_exception', { exception, meta });
// Disable SPA navigation if in SPA mode
// This allows user to navigate away from broken page using normal browser navigation
if (typeof Spa !== 'undefined' && window.rsxapp && window.rsxapp.is_spa) {
Spa.disable();
}
// Show flash alert (rate limited to 1 per second)
const now = Date.now();
if (now - Rsx._last_exception_flash >= 1000) {
Rsx._last_exception_flash = now;
// Determine message based on dev/prod mode
let message;
if (window.rsxapp && window.rsxapp.debug) {
// Dev mode: Show actual error (shortened to 300 chars)
const error_text = error_data.message || 'Unknown error';
message = error_text.length > 300 ? error_text.substring(0, 300) + '...' : error_text;
} else {
// Production mode: Generic message
message = 'An unhandled error has occurred, you may need to refresh your page';
}
// Show flash alert if Flash_Alert is available
if (typeof Flash_Alert !== 'undefined') {
Flash_Alert.error(message);
}
}
}
// Log to server that an event happened
@@ -317,7 +345,19 @@ class Rsx {
// Check if route exists in PHP controller definitions
let pattern;
if (Rsx._routes[class_name] && Rsx._routes[class_name][action_name]) {
pattern = Rsx._routes[class_name][action_name];
const route_patterns = Rsx._routes[class_name][action_name];
// Route patterns are always arrays (even for single routes)
pattern = Rsx._select_best_route_pattern(route_patterns, params_obj);
if (!pattern) {
// Route exists but no pattern satisfies the provided parameters
const route_list = route_patterns.join(', ');
throw new Error(
`No suitable route found for ${class_name}::${action_name} with provided parameters. ` +
`Available routes: ${route_list}`
);
}
} else {
// Not found in PHP routes - check if it's a SPA action
pattern = Rsx._try_spa_action_route(class_name, params_obj);

View File

@@ -2647,6 +2647,25 @@ class Manifest
$method_data['parameters'] = $parameters;
}
// Extract return type if available
$return_type = $method->getReturnType();
if ($return_type !== null) {
if ($return_type instanceof \ReflectionUnionType) {
// Union type (e.g., "array|null")
$method_data['return_type'] = [
'type' => 'union',
'types' => array_map(fn($t) => $t->getName(), $return_type->getTypes()),
'nullable' => $return_type->allowsNull()
];
} else {
// Single type (e.g., "array", "string", "int")
$method_data['return_type'] = [
'type' => $return_type->getName(),
'nullable' => $return_type->allowsNull()
];
}
}
$public_static_methods[$method->getName()] = $method_data;
}
@@ -3173,6 +3192,37 @@ class Manifest
'A method must be either a Route, Ajax_Endpoint, OR Task, not multiple types.'
);
}
// Check Ajax_Endpoint methods don't have return types
if ($has_ajax_endpoint && isset($method_info['return_type'])) {
$class_name = $metadata['class'] ?? 'Unknown';
$return_type_info = $method_info['return_type'];
// Format return type for error message
if (isset($return_type_info['type']) && $return_type_info['type'] === 'union') {
$type_display = implode('|', $return_type_info['types']);
} else {
$type_display = $return_type_info['type'] ?? 'unknown';
if (!empty($return_type_info['nullable'])) {
$type_display = '?' . $type_display;
}
}
throw new \RuntimeException(
"Ajax endpoint has forbidden return type declaration: {$type_display}\n" .
"Class: {$class_name}\n" .
"Method: {$method_name}\n" .
"File: {$file_path}\n\n" .
"Ajax endpoints must NOT declare return types because they need flexibility to return:\n" .
"- Array data (success case)\n" .
"- Form_Error_Response (validation errors)\n" .
"- Redirect_Response (redirects)\n" .
"- Other response types as needed\n\n" .
"Solution: Remove the return type declaration from this method.\n" .
"Change: public static function {$method_name}(...): {$type_display}\n" .
"To: public static function {$method_name}(...)\n"
);
}
}
}
}

View File

@@ -1,28 +0,0 @@
<?php
/**
* CODING CONVENTION:
* This file follows the coding convention where variable_names and function_names
* use snake_case (underscore_wherever_possible).
*/
namespace App\RSpade\Core\Response;
use App\RSpade\Core\Response\Rsx_Response_Abstract;
/**
* Authentication required response
*/
class Auth_Required_Response extends Rsx_Response_Abstract
{
public function __construct(string $reason = "Authentication Required", ?string $redirect = "/login")
{
$this->reason = $reason;
$this->redirect = $redirect;
$this->details = [];
}
public function get_type(): string
{
return 'response_auth_required';
}
}

View File

@@ -0,0 +1,63 @@
<?php
/**
* CODING CONVENTION:
* This file follows the coding convention where variable_names and function_names
* use snake_case (underscore_wherever_possible).
*/
namespace App\RSpade\Core\Response;
use App\RSpade\Core\Ajax\Ajax;
use App\RSpade\Core\Response\Rsx_Response_Abstract;
/**
* Unified error response
*
* Replaces Form_Error_Response, Auth_Required_Response, Unauthorized_Response, etc.
*/
class Error_Response extends Rsx_Response_Abstract
{
protected string $error_code;
protected array $metadata;
public function __construct(string $error_code, $metadata = null)
{
$this->error_code = $error_code;
// Normalize metadata to array
if ($metadata === null) {
$this->metadata = [];
} elseif (is_string($metadata)) {
$this->metadata = ['message' => $metadata];
} elseif (is_array($metadata)) {
$this->metadata = $metadata;
} else {
$this->metadata = ['message' => (string)$metadata];
}
// Set reason from message or use default
if (isset($this->metadata['message'])) {
$this->reason = $this->metadata['message'];
} else {
$this->reason = Ajax::get_default_message($error_code);
}
$this->details = $this->metadata;
$this->redirect = null;
}
public function get_type(): string
{
return $this->error_code;
}
public function get_error_code(): string
{
return $this->error_code;
}
public function get_metadata(): array
{
return $this->metadata;
}
}

View File

@@ -1,28 +0,0 @@
<?php
/**
* CODING CONVENTION:
* This file follows the coding convention where variable_names and function_names
* use snake_case (underscore_wherever_possible).
*/
namespace App\RSpade\Core\Response;
use App\RSpade\Core\Response\Rsx_Response_Abstract;
/**
* Fatal error response
*/
class Fatal_Error_Response extends Rsx_Response_Abstract
{
public function __construct(string $reason = "An error has occurred.", array $details = [])
{
$this->reason = $reason;
$this->redirect = null;
$this->details = $details;
}
public function get_type(): string
{
return 'fatal';
}
}

View File

@@ -1,28 +0,0 @@
<?php
/**
* CODING CONVENTION:
* This file follows the coding convention where variable_names and function_names
* use snake_case (underscore_wherever_possible).
*/
namespace App\RSpade\Core\Response;
use App\RSpade\Core\Response\Rsx_Response_Abstract;
/**
* Form error response
*/
class Form_Error_Response extends Rsx_Response_Abstract
{
public function __construct(string $reason = "An error has occurred.", array $details = [])
{
$this->reason = $reason;
$this->redirect = null;
$this->details = $details;
}
public function get_type(): string
{
return 'response_form_error';
}
}

View File

@@ -1,28 +0,0 @@
<?php
/**
* CODING CONVENTION:
* This file follows the coding convention where variable_names and function_names
* use snake_case (underscore_wherever_possible).
*/
namespace App\RSpade\Core\Response;
use App\RSpade\Core\Response\Rsx_Response_Abstract;
/**
* Unauthorized response
*/
class Unauthorized_Response extends Rsx_Response_Abstract
{
public function __construct(string $reason = "Unauthorized", ?string $redirect = null)
{
$this->reason = $reason;
$this->redirect = $redirect;
$this->details = [];
}
public function get_type(): string
{
return 'response_unauthorized';
}
}

View File

@@ -37,6 +37,9 @@ class Spa {
// Flag to track if SPA is enabled (can be disabled on errors or dirty forms)
static _spa_enabled = true;
// Timer ID for 30-minute auto-disable
static _spa_timeout_timer = null;
/**
* Disable SPA navigation - all navigation becomes full page loads
* Call this when errors occur or forms are dirty
@@ -55,6 +58,52 @@ class Spa {
Spa._spa_enabled = true;
}
/**
* Start 30-minute timeout to auto-disable SPA
* Prevents users from working with stale code for more than 30 minutes
*/
static _start_spa_timeout() {
// 30-minute timeout to auto-disable SPA navigation
//
// WHY: When the application is deployed with updated code, users who have the
// SPA already loaded in their browser will continue using the old JavaScript
// bundle indefinitely. This can cause:
// - API mismatches (stale client code calling updated server endpoints)
// - Missing features or UI changes
// - Bugs from stale client-side logic
//
// FUTURE: A future version of RSpade will use WebSockets to trigger all clients
// to automatically reload their pages on deploy. However, this timeout serves as
// a secondary line of defense against:
// - Failures in the WebSocket notification system
// - Memory leaks in long-running SPA sessions
// - Other unforeseen issues that may arise
// This ensures that users will eventually and periodically get a fresh state,
// regardless of any other system failures.
//
// SOLUTION: After 30 minutes, automatically disable SPA navigation. The next
// forward navigation (link click, manual dispatch) will do a full page reload,
// fetching the new bundle. Back/forward buttons continue to work via SPA
// (force: true) to preserve form state and scroll position.
//
// 30 MINUTES: Chosen as a balance between:
// - Short enough that users don't work with stale code for too long
// - Long enough that users aren't interrupted during active work sessions
//
// TODO: Make this timeout value configurable by developers via:
// - window.rsxapp.spa_timeout_minutes (set in PHP)
// - Default to 30 if not specified
// - Allow 0 to disable timeout entirely (for dev/testing)
const timeout_ms = 30 * 60 * 1000;
Spa._spa_timeout_timer = setTimeout(() => {
console.warn('[Spa] 30-minute timeout reached - disabling SPA navigation');
Spa.disable();
}, timeout_ms);
console_debug('Spa', '30-minute auto-disable timer started');
}
/**
* Framework module initialization hook called during framework boot
* Only runs when window.rsxapp.is_spa === true
@@ -67,6 +116,9 @@ class Spa {
console_debug('Spa', 'Initializing Spa system');
// Start 30-minute auto-disable timer
Spa._start_spa_timeout();
// Discover and register all action classes
Spa.discover_actions();
@@ -324,13 +376,6 @@ class Spa {
// Get target URL (browser has already updated location)
const url = window.location.pathname + window.location.search + window.location.hash;
// If SPA is disabled, still handle back/forward as SPA navigation
// (We can't convert existing history entries to full page loads)
// Only forward navigation (link clicks) will become full page loads
if (!Spa._spa_enabled) {
console_debug('Spa', 'SPA disabled but handling popstate as SPA navigation (back/forward)');
}
// Retrieve scroll position from history state
const scroll = e.state?.scroll || null;
@@ -349,9 +394,11 @@ class Spa {
// const form_data = e.state?.form_data || {};
// Dispatch without modifying history (we're already at the target URL)
// Force SPA dispatch even if disabled - popstate navigates to cached history state
Spa.dispatch(url, {
history: 'none',
scroll: scroll
scroll: scroll,
force: true
});
});
@@ -433,10 +480,12 @@ class Spa {
* - 'none': Don't modify history (used for back/forward)
* @param {object|null} options.scroll - Scroll position {x, y} to restore (default: null = scroll to top)
* @param {boolean} options.triggers - Fire before/after dispatch events (default: true)
* @param {boolean} options.force - Force SPA dispatch even if disabled (used by popstate) (default: false)
*/
static async dispatch(url, options = {}) {
// Check if SPA is disabled - do full page load
if (!Spa._spa_enabled) {
// Exception: popstate events always attempt SPA dispatch (force: true)
if (!Spa._spa_enabled && !options.force) {
console.warn('[Spa.dispatch] SPA disabled, forcing full page load');
document.location.href = url;
return;
@@ -621,12 +670,18 @@ class Spa {
Spa.layout.stop();
}
// Clear body and create new layout
$('body').empty();
$('body').attr('class', '');
// Clear spa-root and create new layout
// Note: We target #spa-root instead of body to preserve global UI containers
// (Flash_Alert, modals, tooltips, etc. that append to body)
const $spa_root = $('#spa-root');
if (!$spa_root.length) {
throw new Error('[Spa] #spa-root element not found - check Spa_App.blade.php');
}
$spa_root.empty();
$spa_root.attr('class', '');
// Create layout using component system
Spa.layout = $('body').component(layout_name, {}).component();
Spa.layout = $spa_root.component(layout_name, {}).component();
// Wait for layout to be ready
await Spa.layout.ready();

View File

@@ -7,12 +7,12 @@
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<meta content="ie=edge" http-equiv="X-UA-Compatible">
{{-- Bundle includes --}}
{!! Frontend_Bundle::render() !!}
{{-- Bundle includes - dynamically rendered based on SPA controller --}}
{!! $bundle::render() !!}
</head>
<body class="{{ rsx_body_class() }}">
<div id="spa-root"></div>
</body>
</html>

View File

@@ -26,7 +26,53 @@ class Spa_Layout extends Component {
* @returns {jQuery} The content element
*/
$content() {
return this.$id('content');
return this.$sid('content');
}
/**
* Show a debug exception message at the top of the content area
* Prepends a styled error box with the exception message
*
* @param {string|Error} exception - Error message or Error object to display
*/
show_debug_exception(exception) {
const $content = this.$content();
if (!$content || !$content.length) {
console.error('[Spa_Layout] Cannot show debug exception: content element not found');
return;
}
// Extract message from Error object or use string directly
let message;
if (exception instanceof Error) {
message = exception.message;
} else if (typeof exception === 'string') {
message = exception;
} else if (exception && typeof exception === 'object' && exception.message) {
message = exception.message;
} else {
message = String(exception);
}
// Create error box with inline styles (no framework dependencies)
const error_html = `
<div style="border: 2px solid #dc3545; background-color: #ffe6e6; color: #000; padding: 15px; margin-bottom: 20px;">
<strong>Fatal Error:</strong> ${this._escape_html(message)}
</div>
`;
// Prepend to content area
$content.prepend(error_html);
}
/**
* Escape HTML to prevent XSS
* @private
*/
_escape_html(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
@@ -77,6 +123,9 @@ class Spa_Layout extends Component {
// Clear content area
$content.empty();
// Mark action as loading for exception display logic
this._action_is_loading = true;
try {
// Get the action class to check for @title decorator
const action_class = Manifest.get_class_by_name(action_name);
@@ -110,20 +159,15 @@ class Spa_Layout extends Component {
// Wait for action to be ready
await action.ready();
} catch (error) {
// Action lifecycle failed - log error, trigger event, disable SPA, show error UI
console.error('[Spa_Layout] Action lifecycle failed:', error);
// Trigger global exception event (goes to Debugger for server logging, Flash_Alert, etc)
if (typeof Rsx !== 'undefined') {
Rsx.trigger('unhandled_exception', {
message: error.message,
stack: error.stack,
type: 'action_lifecycle_error',
action_name: action_name,
error: error,
});
}
// Mark action as done loading
this._action_is_loading = false;
} catch (error) {
// Mark action as done loading (even though it failed)
this._action_is_loading = false;
// Action lifecycle failed - log and trigger event
console.error('[Spa_Layout] Action lifecycle failed:', error);
// Disable SPA so forward navigation becomes full page loads
// (Back/forward still work as SPA to allow user to navigate away)
@@ -131,16 +175,12 @@ class Spa_Layout extends Component {
Spa.disable();
}
// Show error UI in content area so user can navigate away
$content.html(`
<div class="alert alert-danger m-4">
<h4>Page Failed to Load</h4>
<p>An error occurred while loading this page. You can navigate back or to another page.</p>
<p class="mb-0">
<a href="javascript:history.back()" class="btn btn-secondary">Go Back</a>
</p>
</div>
`);
// Trigger global exception event
// Exception_Handler will decide how to display (layout or flash alert)
// Pass payload with exception (no meta.source = already logged above)
if (typeof Rsx !== 'undefined') {
Rsx.trigger('unhandled_exception', { exception: error, meta: {} });
}
// Don't re-throw - allow navigation to continue working
}

View File

@@ -67,7 +67,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
* Generate random first name
*/
#[Ajax_Endpoint]
public static function first_name(Request $request, array $params = []): string
public static function first_name(Request $request, array $params = [])
{
self::__check_production();
return self::__random_from_list('first_names');
@@ -77,7 +77,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
* Generate random last name
*/
#[Ajax_Endpoint]
public static function last_name(Request $request, array $params = []): string
public static function last_name(Request $request, array $params = [])
{
self::__check_production();
return self::__random_from_list('last_names');
@@ -87,7 +87,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
* Generate random company name
*/
#[Ajax_Endpoint]
public static function company_name(Request $request, array $params = []): string
public static function company_name(Request $request, array $params = [])
{
self::__check_production();
@@ -109,7 +109,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
* Generate random street address
*/
#[Ajax_Endpoint]
public static function address(Request $request, array $params = []): string
public static function address(Request $request, array $params = [])
{
self::__check_production();
@@ -124,7 +124,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
* Generate random city name
*/
#[Ajax_Endpoint]
public static function city(Request $request, array $params = []): string
public static function city(Request $request, array $params = [])
{
self::__check_production();
return self::__random_from_list('cities');
@@ -134,7 +134,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
* Generate random US state code
*/
#[Ajax_Endpoint]
public static function state(Request $request, array $params = []): string
public static function state(Request $request, array $params = [])
{
self::__check_production();
@@ -153,7 +153,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
* Generate random ZIP code
*/
#[Ajax_Endpoint]
public static function zip(Request $request, array $params = []): string
public static function zip(Request $request, array $params = [])
{
self::__check_production();
return str_pad((string)rand(10000, 99999), 5, '0', STR_PAD_LEFT);
@@ -164,7 +164,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
* Avoids 555-000-0000 and any sequence containing 911
*/
#[Ajax_Endpoint]
public static function phone(Request $request, array $params = []): string
public static function phone(Request $request, array $params = [])
{
self::__check_production();
@@ -193,7 +193,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
* Format: wordwordnumbers+test@gmail.com
*/
#[Ajax_Endpoint]
public static function email(Request $request, array $params = []): string
public static function email(Request $request, array $params = [])
{
self::__check_production();
@@ -208,7 +208,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
* Generate random website URL
*/
#[Ajax_Endpoint]
public static function website(Request $request, array $params = []): string
public static function website(Request $request, array $params = [])
{
self::__check_production();
@@ -223,7 +223,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
* Generate random text/paragraph
*/
#[Ajax_Endpoint]
public static function text(Request $request, array $params = []): string
public static function text(Request $request, array $params = [])
{
self::__check_production();

View File

@@ -1060,66 +1060,15 @@ function rsx_body_class()
}
/**
* Create an authentication required response
* Create a unified error response
*
* Returns a special response object that Dispatch and Ajax_Endpoint handle differently:
* - HTTP requests: Sets flash alert and redirects to login
* - AJAX requests: Returns JSON error with success:false
*
* @param string $reason The reason authentication is required
* @param string|null $redirect The URL to redirect to (default: /login)
* @return \App\RSpade\Core\Response\Auth_Required_Response
* @param string $error_code One of Ajax::ERROR_* constants
* @param string|array|null $metadata Error message (string) or structured data (array)
* @return \App\RSpade\Core\Response\Error_Response
*/
function response_auth_required($reason = 'Authentication Required', $redirect = '/login')
function response_error(string $error_code, $metadata = null)
{
return new \App\RSpade\Core\Response\Auth_Required_Response($reason, $redirect);
}
/**
* Create an unauthorized response
*
* Returns a special response object that Dispatch and Ajax_Endpoint handle differently:
* - HTTP requests: Sets flash alert and redirects or throws exception
* - AJAX requests: Returns JSON error with success:false
*
* @param string $reason The reason for unauthorized access
* @param string|null $redirect The URL to redirect to (null throws exception)
* @return \App\RSpade\Core\Response\Unauthorized_Response
*/
function response_unauthorized($reason = 'Unauthorized', $redirect = null)
{
return new \App\RSpade\Core\Response\Unauthorized_Response($reason, $redirect);
}
/**
* Create a form error response
*
* Returns a special response object that Dispatch and Ajax_Endpoint handle differently:
* - HTTP requests: Sets flash alert and redirects back to same URL as GET
* - AJAX requests: Returns JSON error with success:false and details
*
* @param string $reason The error message
* @param array $details Additional error details
* @return \App\RSpade\Core\Response\Form_Error_Response
*/
function response_form_error($reason = 'An error has occurred.', $details = [])
{
return new \App\RSpade\Core\Response\Form_Error_Response($reason, $details);
}
/**
* Create a fatal error response
*
* Returns a special response object that always throws an exception
* in both HTTP and AJAX contexts
*
* @param string $reason The error message
* @param array $details Additional error details
* @return \App\RSpade\Core\Response\Fatal_Error_Response
*/
function response_fatal_error($reason = 'An error has occurred.', $details = [])
{
return new \App\RSpade\Core\Response\Fatal_Error_Response($reason, $details);
return new \App\RSpade\Core\Response\Error_Response($error_code, $metadata);
}
/**

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)

View File

@@ -90,6 +90,63 @@ class Frontend_Clients_Edit {
---
## 2025-11-20: Unified Ajax Error Response System
**Component**: Ajax error handling
**Impact**: Medium - Existing error handling code continues to work, but new pattern recommended
### Change
Replaced fragmented error response helpers (`response_form_error()`, `response_auth_required()`, etc.) with unified `response_error()` function using constants for error codes. Same constant names on server and client for zero mental translation.
### Previous Pattern
```php
// Server - different functions for each error type
return response_form_error('Validation failed', ['email' => 'Invalid']);
return response_not_found('Project not found');
return response_unauthorized('Permission denied');
```
```javascript
// Client - string matching with different names
if (error.type === 'form_error') { ... }
if (error.type === 'not_found') { ... }
```
### New Pattern
```php
// Server - single function with constants
return response_error(Ajax::ERROR_VALIDATION, ['email' => 'Invalid']);
return response_error(Ajax::ERROR_NOT_FOUND, 'Project not found');
return response_error(Ajax::ERROR_UNAUTHORIZED); // Auto-message
```
```javascript
// Client - same constant names
if (e.code === Ajax.ERROR_VALIDATION) { ... }
if (e.code === Ajax.ERROR_NOT_FOUND) { ... }
```
### Migration Steps
1. **Update server-side error responses** to use `response_error()` with constants
2. **Update client-side error checks** to use `e.code === Ajax.ERROR_*` instead of `e.type === 'string'`
3. **Read updated documentation**: `php artisan rsx:man ajax_error_handling`
Old helpers still work but are deprecated. Framework code being updated to new pattern.
### Benefits
- Same constant names server and client (`Ajax::ERROR_NOT_FOUND` = `Ajax.ERROR_NOT_FOUND`)
- IDE autocomplete for error codes
- Refactor-safe (rename constant updates both sides)
- Auto-generated messages for common errors
- Simpler API (one function instead of many)
---
## Template for Future Entries
```

View File

@@ -26,6 +26,11 @@ This separation ensures:
**PURPOSE**: Essential directives for AI/LLM assistants developing RSX applications with RSpade.
## CRITICAL: Questions vs Commands
- **Questions get answers, NOT actions** - "Is that fire?" gets "Yes" not "Let me run through it". User has a plan, don't take destructive action when asked a question.
- **Commands get implementation** - Clear directives result in code changes
## What is RSpade?
**Visual Basic-like development for PHP/Laravel.** Think: VB6 apps → VB6 runtime → Windows = RSX apps → RSpade runtime → Laravel.
@@ -314,7 +319,7 @@ class Frontend_Layout extends Spa_Layout {
}
```
Layout template must have `$id="content"` element where actions render.
Layout template must have `$sid="content"` element where actions render.
### URL Generation & Navigation
@@ -363,6 +368,30 @@ class Contacts_View_Action extends Spa_Action {
Details: `php artisan rsx:man spa`
### View Action Pattern (Loading Data)
For SPA actions that load data (view/edit CRUD pages), use the three-state pattern:
```javascript
on_create() {
this.data.record = { name: '' }; // Stub prevents undefined errors
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 uses three states: `<Loading_Spinner>``<Universal_Error_Page_Component>` → content.
**Details**: `php artisan rsx:man view_action_patterns`
---
## CONVERTING BLADE PAGES TO SPA ACTIONS
@@ -415,7 +444,7 @@ class Feature_Index_Action extends Spa_Action {
```php
// Remove #[Route] method completely. Add Ajax endpoints:
#[Ajax_Endpoint]
public static function fetch_items(Request $request, array $params = []): array {
public static function fetch_items(Request $request, array $params = []) {
return ['items' => Feature_Model::all()];
}
```
@@ -615,6 +644,10 @@ For mechanical thinkers who see structure, not visuals. Write `<User_Card>` not
directly in attribute context. Works with static values, interpolations, and multiple conditions per element.
Example: `<input <% if (this.args.required) { %>required="required"<% } %> />`
**Inline Logic**: `<% this.handler = () => action(); %>` then `@click=this.handler` - No JS file needed for simple components
**Event Handlers**: `@click=this.method` (unquoted) - Methods defined inline or in companion .js
**Validation**: `<% if (!this.args.required) throw new Error('Missing arg'); %>` - Fail loud in template
### 🔴 State Management Rules (ENFORCED)
**this.args** - Component arguments (read-only in on_load(), modifiable elsewhere)
@@ -702,21 +735,21 @@ async on_load() {
- **`$quoted="string"`** → String literal
- **`$unquoted=expression`** → JavaScript expression
- **`$id="name"`** → Scoped element ID
- **`$sid="name"`** → Scoped element ID
### Component Access
**this.$id(name)** → jQuery object (for DOM):
**this.$sid(name)** → jQuery object (for DOM):
```javascript
this.$id('button').on('click', ...);
this.$sid('button').on('click', ...);
```
**this.id(name)** → Component instance (for methods):
**this.sid(name)** → Component instance (for methods):
```javascript
const comp = this.id('child'); // ✅ Returns component
const comp = this.sid('child'); // ✅ Returns component
await comp.reload();
const comp = this.id('child').component(); // ❌ WRONG
const comp = this.sid('child').component(); // ❌ WRONG
```
### Incremental Scaffolding
@@ -741,7 +774,7 @@ const comp = this.id('child').component(); // ❌ WRONG
7. Use `Controller.method()` not `$.ajax()`
8. Blade components self-closing only
9. `on_create/render/stop` must be sync
10. Use this.id() for components, NOT this.id().component()
10. Use this.sid() for components, NOT this.sid().component()
### Bundle Integration Required
@@ -786,11 +819,11 @@ class My_Form extends Component {
vals(values) {
if (values) {
// Setter - populate form
this.$id('name').val(values.name || '');
this.$sid('name').val(values.name || '');
return null;
} else {
// Getter - extract values
return {name: this.$id('name').val()};
return {name: this.$sid('name').val()};
}
}
}
@@ -829,7 +862,7 @@ const result = await Modal.form({
});
```
**Requirements**: Form component must implement `vals()` and include `<div $id="error_container"></div>`.
**Requirements**: Form component must implement `vals()` and include `<div $sid="error_container"></div>`.
**Modal Classes** (for complex/reusable modals):
```javascript
@@ -963,47 +996,49 @@ Details: `php artisan rsx:man file_upload`
## AJAX ENDPOINTS
**Return data directly - framework auto-wraps as `{success: true, data: ...}`**
```php
#[Ajax_Endpoint]
public static function fetch_client(Request $request, array $params = []) {
$client = Client_Model::find($params['id']);
if (!$client) throw new \Exception('Not found');
return $client; // ✅ Return data directly
public static function method(Request $request, array $params = []) {
return $data; // Success - framework wraps as {_success: true, _ajax_return_value: ...}
}
```
**❌ NEVER**: `return ['success' => true, 'data' => $client]` - framework adds this
**Call:** `await Controller.method({param: value})`
**Special returns** (bypass auto-wrap):
- `['errors' => [...]]` - Form validation errors
- `['redirect' => '/path']` - Client-side navigation
- `['error' => 'msg']` - Operation failure
### Error Responses
Use `response_error(Ajax::ERROR_CODE, $metadata)`:
```php
// Form validation
if (empty($params['name'])) {
return ['errors' => ['name' => 'Required']];
}
// Not found
return response_error(Ajax::ERROR_NOT_FOUND, 'Project not found');
// Success with redirect
Flash_Alert::success('Saved');
return ['redirect' => Rsx::Route('View_Action', $id)];
// Validation
return response_error(Ajax::ERROR_VALIDATION, [
'email' => 'Invalid',
'name' => 'Required'
]);
// Permission error
if (!can_delete()) {
return ['error' => 'Permission denied'];
// Auto-message
return response_error(Ajax::ERROR_UNAUTHORIZED);
```
**Codes:** `ERROR_VALIDATION`, `ERROR_NOT_FOUND`, `ERROR_UNAUTHORIZED`, `ERROR_AUTH_REQUIRED`, `ERROR_FATAL`, `ERROR_GENERIC`
**Client:**
```javascript
try {
const data = await Controller.get(id);
} catch (e) {
if (e.code === Ajax.ERROR_NOT_FOUND) {
// Handle
} else {
alert(e.message); // Generic
}
}
```
---
### Flash Alerts in Ajax Endpoints
Use server-side success alerts ONLY with redirects (no on-screen element to show message).
**Client-side**: Use `Flash_Alert.error()`, `Flash_Alert.warning()` on case-by-case basis.
Unhandled errors auto-show flash alert.
---

50
node_modules/.package-lock.json generated vendored
View File

@@ -2211,9 +2211,9 @@
}
},
"node_modules/@jqhtml/core": {
"version": "2.2.217",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/core/-/core-2.2.217.tgz",
"integrity": "sha512-rmu7jgRM3PPvKGkFbRZ0wTXBxHPuvVf9aIMSXv6n0KceTvKlqLR2EFHjPrHbCp9X0DyzoooiVhKEJcKlbVIXJw==",
"version": "2.2.218",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/core/-/core-2.2.218.tgz",
"integrity": "sha512-CEbrpoi70Y5ET1fBXHK38fZTi5MAtZEY2c779mwjw3Dn7SjpoxvCYet/AJLJI7lngA6eJl9BPaPwALMHbafkLQ==",
"license": "MIT",
"dependencies": {
"@rollup/plugin-node-resolve": "^16.0.1",
@@ -2237,9 +2237,9 @@
}
},
"node_modules/@jqhtml/parser": {
"version": "2.2.217",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/parser/-/parser-2.2.217.tgz",
"integrity": "sha512-pAQegkFNZSY+DjkrvQ162c78lg9YU2gCaftYxWdLTrKP86mr0QVG8vAQ8IsvH/sQBvVRUIR9sNSjK+fXd/SnlQ==",
"version": "2.2.218",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/parser/-/parser-2.2.218.tgz",
"integrity": "sha512-i8Y/tx/mIwhAw8YZd99j2i8iLWSU/9Ua88u4p0h3sCCeIWGT7QGvrr7vF+OiUcb8ECtX1188ODlHWdWt7vb45w==",
"license": "MIT",
"dependencies": {
"@types/jest": "^29.5.11",
@@ -2257,9 +2257,9 @@
}
},
"node_modules/@jqhtml/router": {
"version": "2.2.217",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/router/-/router-2.2.217.tgz",
"integrity": "sha512-WiUAOb92sDY3kRD8lmsxcd8oSNqVmCDMsxZj0z/KRLCa0Y6kGrtk8AlQiunBQzvEHGVIb/Kt/6P1WhKEKaBh/g==",
"version": "2.2.218",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/router/-/router-2.2.218.tgz",
"integrity": "sha512-aQum/FdDlqbtNbtkIJFN5sGNTBhlGBn5duclsyv0CYmJ8ruC2Gr0y5FILBeuc1lFSmG/6UJZ+eOlrQ4QDk2zng==",
"license": "MIT",
"dependencies": {
"@rollup/plugin-node-resolve": "^16.0.1",
@@ -2277,21 +2277,21 @@
}
},
"node_modules/@jqhtml/vscode-extension": {
"version": "2.2.217",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/vscode-extension/-/vscode-extension-2.2.217.tgz",
"integrity": "sha512-6AXZnG03DFi78tdCv21dQF5ILt/YCVPGqZpFKIBNH+CaNccCFW415fMvfC6jNhm34fUSMu0MCHmWSGXzamCtGQ==",
"version": "2.2.218",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/vscode-extension/-/vscode-extension-2.2.218.tgz",
"integrity": "sha512-fEOcYqi2AVkLxxJ3ovwKqJQGpLI9C6sgcRWs3HVuDH6UYpNiRPUwzSxf/M7j+wZY5y5tt7KGSDK5JjbSj0PqqQ==",
"license": "MIT",
"engines": {
"vscode": "^1.74.0"
}
},
"node_modules/@jqhtml/webpack-loader": {
"version": "2.2.217",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/webpack-loader/-/webpack-loader-2.2.217.tgz",
"integrity": "sha512-zUquYIBEEodMp5xypVbR7dbrsaII6Ojux5i93HS68sMfWzafXr67mEGyjv9ls0sZ47SD8L37iQef5sh3OVRxww==",
"version": "2.2.218",
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/webpack-loader/-/webpack-loader-2.2.218.tgz",
"integrity": "sha512-QN/4qTsxPjB9OWHdNVvif05ygw9FYblF6KYyHPvdZ0NNWNoUMYBcKizjjTjFAsUMqWQkT4RETx5XxkiomgKPPQ==",
"license": "MIT",
"dependencies": {
"@jqhtml/parser": "2.2.217",
"@jqhtml/parser": "2.2.218",
"@types/loader-utils": "^2.0.6",
"@types/node": "^20.0.0",
"@types/webpack": "^5.28.5",
@@ -4017,9 +4017,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.8.29",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.29.tgz",
"integrity": "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA==",
"version": "2.8.30",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.30.tgz",
"integrity": "sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
@@ -5697,9 +5697,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.257",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.257.tgz",
"integrity": "sha512-VNSOB6JZan5IQNMqaurYpZC4bDPXcvKlUwVD/ztMeVD7SwOpMYGOY7dgt+4lNiIHIpvv/FdULnZKqKEy2KcuHQ==",
"version": "1.5.259",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.259.tgz",
"integrity": "sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ==",
"license": "ISC"
},
"node_modules/elliptic": {
@@ -10841,9 +10841,9 @@
"license": "MIT"
},
"node_modules/sass": {
"version": "1.94.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.94.1.tgz",
"integrity": "sha512-/YVm5FRQaRlr3oNh2LLFYne1PdPlRZGyKnHh1sLleOqLcohTR4eUUvBjBIqkl1fEXd1MGOHgzJGJh+LgTtV4KQ==",
"version": "1.94.2",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.94.2.tgz",
"integrity": "sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==",
"license": "MIT",
"dependencies": {
"chokidar": "^4.0.0",

View File

@@ -27,7 +27,7 @@
- Access component methods via `this.methodName()`
### Attribute System
- `$id="name"``id="name:_cid"` (component-scoped ID)
- `$sid="name"``id="name:_cid"` (component-scoped ID)
- `$attr="value"``data-attr="value"` (data attributes)
- `@click="handler"` → jQuery event binding to component method
- `@submit.prevent="handler"` → Event with preventDefault
@@ -217,7 +217,7 @@ class UserCard extends Jqhtml_Component {
- `this._ready_state` - Lifecycle phase (0=created, 1=rendering, 2=creating, 3=loading, 4=ready)
### DOM Access Methods
- `this.$id('name')` - Get element by scoped ID (returns jQuery object)
- `this.$sid('name')` - Get element by scoped ID (returns jQuery object)
- `this.$child('name')` - Get child component by name
- `this.$children()` - Get all direct child components
- `this.$parent()` - Get parent component
@@ -260,11 +260,11 @@ class CustomInput extends Component {
val(value) {
if (arguments.length === 0) {
// Getter - return processed value
return this.parse_value(this.$id('input').val());
return this.parse_value(this.$sid('input').val());
} else {
// Setter - validate and set
if (this.validate(value)) {
this.$id('input').val(this.format_value(value));
this.$sid('input').val(this.format_value(value));
this.data.value = value;
}
return this.$; // Maintain jQuery chaining

20
node_modules/@jqhtml/core/README.md generated vendored
View File

@@ -132,7 +132,7 @@ If you need to show different DOM states before and after loading, use these pat
class DataComponent extends Component {
async on_create() {
// Set loading state in the DOM during create phase
this.$id('status').addClass('loading').text('Loading...');
this.$sid('status').addClass('loading').text('Loading...');
}
async on_load() {
@@ -142,8 +142,8 @@ class DataComponent extends Component {
async on_ready() {
// Update DOM after data is loaded
this.$id('status').removeClass('loading').text('Loaded');
this.$id('username').text(this.data.user.name);
this.$sid('status').removeClass('loading').text('Loaded');
this.$sid('username').text(this.data.user.name);
}
}
```
@@ -153,12 +153,12 @@ class DataComponent extends Component {
class BadComponent extends Component {
async on_load() {
// ❌ WRONG - DOM modification in load()
this.$id('status').text('Loading...'); // VIOLATION!
this.$sid('status').text('Loading...'); // VIOLATION!
this.data.user = await fetch('/api/user').then(r => r.json());
// ❌ WRONG - More DOM modification
this.$id('status').text('Loaded'); // VIOLATION!
this.$sid('status').text('Loaded'); // VIOLATION!
}
}
```
@@ -261,9 +261,9 @@ class TabsComponent extends Component {
selectTab(tabId) {
// Use $id() for scoped selection
this.$id('tab1').removeClass('active');
this.$id('tab2').removeClass('active');
this.$id(tabId).addClass('active');
this.$sid('tab1').removeClass('active');
this.$sid('tab2').removeClass('active');
this.$sid(tabId).addClass('active');
}
}
```
@@ -300,7 +300,7 @@ JQHTML has specific rules for attribute quoting and value passing:
- **@ Event attributes**: MUST be unquoted (pass function references)
Example: `@click=this.handleClick`
- **$ Data attributes**: Can be quoted OR unquoted (flexible)
Example: `$id="my-id"` or `$data=this.complexObject`
Example: `$sid="my-id"` or `$data=this.complexObject`
- **Regular HTML attributes**: MUST be quoted (strings only)
Example: `class="container <%= this.args.theme %>"`
@@ -375,7 +375,7 @@ $('#user-profile').component().on('ready', (component) => {
console.log('User data:', component.data.user);
// Safe to access all DOM elements
component.$id('email').addClass('verified');
component.$sid('email').addClass('verified');
});
```

0
node_modules/@jqhtml/core/dist/component-registry.d.ts generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/core/dist/component-registry.d.ts.map generated vendored Executable file → Normal file
View File

28
node_modules/@jqhtml/core/dist/component.d.ts generated vendored Executable file → Normal file
View File

@@ -193,6 +193,17 @@ export declare class Jqhtml_Component {
on_load(): Promise<void>;
on_ready(): Promise<void>;
on_stop(): void | Promise<void>;
/**
* Optional: Override cache key generation
*
* By default, cache keys are generated from component name + args.
* Override this method to provide a custom cache key for this component instance.
*
* If this method throws an error, caching will be disabled for this component.
*
* @returns Custom cache key string (will be prefixed with component name)
*/
cache_id?(): string;
/**
* Should component re-render after load?
* By default, only re-renders if data has changed
@@ -231,9 +242,9 @@ export declare class Jqhtml_Component {
* Searches for elements with id="local_id:THIS_COMPONENT_CID"
*
* Example:
* Template: <button $id="save_btn">Save</button>
* Rendered: <button id="save_btn:abc123" data-id="save_btn">Save</button>
* Access: this.$id('save_btn') // Returns jQuery element
* Template: <button $sid="save_btn">Save</button>
* Rendered: <button id="save_btn:abc123" data-sid="save_btn">Save</button>
* Access: this.$sid('save_btn') // Returns jQuery element
*
* Performance: Uses native document.getElementById() when component is in DOM,
* falls back to jQuery.find() for components not yet attached to DOM.
@@ -241,21 +252,24 @@ export declare class Jqhtml_Component {
* @param local_id The local ID (without _cid suffix)
* @returns jQuery element with id="local_id:_cid", or empty jQuery object if not found
*/
$id(local_id: string): any;
$sid(local_id: string): any;
/**
* Get component instance by scoped ID
*
* Convenience method that finds element by scoped ID and returns the component instance.
*
* Example:
* Template: <User_Card $id="active_user" />
* Access: const user = this.id('active_user'); // Returns User_Card instance
* Template: <User_Card $sid="active_user" />
* Access: const user = this.sid('active_user'); // Returns User_Card instance
* user.data.name // Access component's data
*
* To get the scoped ID string itself:
* this.$sid('active_user').attr('id') // Returns "active_user:abc123xyz"
*
* @param local_id The local ID (without _cid suffix)
* @returns Component instance or null if not found or not a component
*/
id(local_id: string): Jqhtml_Component | null;
sid(local_id: string): Jqhtml_Component | null;
/**
* Get the component that instantiated this component (rendered it in their template)
* Returns null if component was created programmatically via $().component()

2
node_modules/@jqhtml/core/dist/component.d.ts.map generated vendored Executable file → Normal file
View File

@@ -1 +1 @@
{"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAUH,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,YAAY,CAAC,EAAE;YACb,GAAG,EAAE,CAAC,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC;YACjF,UAAU,EAAE,MAAM,IAAI,CAAC;SACxB,CAAC;KACH;CACF;AAED,qBAAa,gBAAgB;IAE3B,MAAM,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC;IAGtB,CAAC,EAAE,GAAG,CAAC;IACP,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAK;IAGzB,OAAO,CAAC,kBAAkB,CAAmB;IAC7C,OAAO,CAAC,aAAa,CAAiC;IACtD,OAAO,CAAC,WAAW,CAAiC;IACpD,OAAO,CAAC,aAAa,CAAoC;IACzD,OAAO,CAAC,iBAAiB,CAAkB;IAC3C,OAAO,CAAC,QAAQ,CAAkB;IAClC,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,mBAAmB,CAAuB;IAClD,OAAO,CAAC,oBAAoB,CAAwE;IACpG,OAAO,CAAC,iBAAiB,CAA0B;IACnD,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,iBAAiB,CAAkB;IAC3C,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,oBAAoB,CAAoC;IAChE,OAAO,CAAC,oBAAoB,CAAuB;IACnD,OAAO,CAAC,uBAAuB,CAAoC;IACnE,OAAO,CAAC,aAAa,CAAkB;IACvC,OAAO,CAAC,iBAAiB,CAAC,CAAsB;IAChD,OAAO,CAAC,yBAAyB,CAAwB;gBAE7C,OAAO,CAAC,EAAE,GAAG,EAAE,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM;IA8IzD;;;OAGG;IACH;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAY5B;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,GAAE,MAAM,GAAG,IAAW,GAAG,MAAM;IA6QzC;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,EAAE,GAAE,MAAM,GAAG,IAAW,GAAG,IAAI;IA+CtC;;;OAGG;IACH,MAAM,CAAC,EAAE,GAAE,MAAM,GAAG,IAAW,GAAG,IAAI;IAItC;;;OAGG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAgF7B;;;;;OAKG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAwQ5B;;;;OAIG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAmB7B;;;;;;;;;;OAUG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAYtB;;;;OAIG;YACW,wBAAwB;IA6BtC;;;;;;;;OAQG;IACG,MAAM,CAAC,aAAa,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAsBpD;;;;;;;;OAQG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAI9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAiCG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IA2I9B;;;;OAIG;IACH;;;;OAIG;IACH,KAAK,IAAI,IAAI;IAkDb;;;OAGG;IACH,IAAI,IAAI,IAAI;IAkBZ,SAAS,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IACjC,SAAS,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAC3B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IACxB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAC/B,OAAO,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAE/B;;;;OAIG;IACH;;;OAGG;IACH,gBAAgB,IAAI,OAAO;IAiB3B;;OAEG;IACH,cAAc,IAAI,MAAM;IAIxB;;;;;;OAMG;IACH,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,SAAS,EAAE,gBAAgB,KAAK,IAAI,GAAG,IAAI;IAsB7E;;;OAGG;IACH,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAiBjC;;;OAGG;IACH,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAK3C;;;;;;;;;;;;;;;OAeG;IACH,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,GAAG;IAgB1B;;;;;;;;;;;;OAYG;IACH,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI;IAgB7C;;;OAGG;IACH,YAAY,IAAI,gBAAgB,GAAG,IAAI;IAIvC;;OAEG;IACH,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,EAAE;IAa1C;;OAEG;IACH,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI;IAoBlD;;OAEG;IACH,MAAM,CAAC,mBAAmB,IAAI,MAAM,EAAE;IA0CtC,OAAO,CAAC,aAAa;IAIrB;;;OAGG;IACH,OAAO,CAAC,qBAAqB;IAkB7B,OAAO,CAAC,kBAAkB;IA4B1B,OAAO,CAAC,yBAAyB;IAuHjC,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,gBAAgB;IAcxB;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IA+BzB,OAAO,CAAC,cAAc;IActB,OAAO,CAAC,UAAU;IAUlB;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,0BAA0B;CAqEnC"}
{"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"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"}

0
node_modules/@jqhtml/core/dist/debug-entry.d.ts generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/core/dist/debug-entry.d.ts.map generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/core/dist/debug-overlay.d.ts generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/core/dist/debug-overlay.d.ts.map generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/core/dist/debug.d.ts generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/core/dist/debug.d.ts.map generated vendored Executable file → Normal file
View File

130
node_modules/@jqhtml/core/dist/index.cjs generated vendored Executable file → Normal file
View File

@@ -499,7 +499,7 @@ function process_tag_to_html(instruction, html, tagElements, components, context
if (key === 'id' && tid) {
// Special handling for id attribute - scope to parent component's _cid
// This is for regular id="foo" attributes that need scoping (rare case)
// Most scoping happens via $id attribute which becomes data-id
// Most scoping happens via $sid attribute which becomes data-sid
// Don't double-scope if already scoped (contains :)
if (typeof value === 'string' && value.includes(':')) {
html.push(` id="${value}"`);
@@ -549,13 +549,13 @@ function process_component_to_html(instruction, html, components, context) {
// Create element with tracking ID
html.push(`<${tagName} data-cid="${cid}"`);
// Handle id attributes for components
// The compiled code always generates both 'id' (scoped) and 'data-id' (base) for $id attributes
// The compiled code always generates both 'id' (scoped) and 'data-sid' (base) for $sid attributes
// We just pass through what the compiler gave us - NEVER regenerate
if (props['data-id']) {
const baseId = props['data-id'];
if (props['data-sid']) {
const baseId = props['data-sid'];
// The compiled code ALWAYS sets props['id'] with the correct scoped value
// Just use it directly - it already has the correct parent _cid baked in
html.push(` id="${props['id']}" data-id="${baseId}"`);
html.push(` id="${props['id']}" data-sid="${baseId}"`);
}
// Regular id passes through unchanged
else if (props['id']) {
@@ -1229,16 +1229,16 @@ class Jqhtml_Component {
// If id provided, delegate to child component
if (id) {
// First check if element with scoped ID exists
const $element = this.$id(id);
const $element = this.$sid(id);
if ($element.length === 0) {
throw new Error(`[JQHTML] render("${id}") - no such id.\n` +
`Component "${this.component_name()}" has no child element with $id="${id}".`);
`Component "${this.component_name()}" has no child element with $sid="${id}".`);
}
// Element exists, check if it's a component
const child = $element.data('_component');
if (!child) {
throw new Error(`[JQHTML] render("${id}") - element is not a component or does not have $redrawable attribute set.\n` +
`Element with $id="${id}" exists but is not initialized as a component.\n` +
`Element with $sid="${id}" exists but is not initialized as a component.\n` +
`Add $redrawable attribute or make it a proper component.`);
}
return child._render();
@@ -1459,15 +1459,15 @@ class Jqhtml_Component {
return;
// If id provided, delegate to child component
if (id) {
const $element = this.$id(id);
const $element = this.$sid(id);
if ($element.length === 0) {
throw new Error(`[JQHTML] render("${id}") - no such id.\n` +
`Component "${this.component_name()}" has no child element with $id="${id}".`);
`Component "${this.component_name()}" has no child element with $sid="${id}".`);
}
const child = $element.data('_component');
if (!child) {
throw new Error(`[JQHTML] render("${id}") - element is not a component or does not have $redrawable attribute set.\n` +
`Element with $id="${id}" exists but is not initialized as a component.\n` +
`Element with $sid="${id}" exists but is not initialized as a component.\n` +
`Add $redrawable attribute or make it a proper component.`);
}
return child.render();
@@ -1515,8 +1515,26 @@ class Jqhtml_Component {
// This happens after on_create() but before render, allowing instant first render with cached data
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
const { key: cache_key, uncacheable_property } = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
// If cache_key is null, args are not serializable - skip caching
// Check if component implements cache_id() for custom cache key
let cache_key = null;
let uncacheable_property;
if (typeof this.cache_id === 'function') {
try {
const custom_cache_id = this.cache_id();
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
}
catch (error) {
// cache_id() threw error - disable caching
uncacheable_property = 'cache_id()';
}
}
else {
// Use standard args-based cache key generation
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
cache_key = result.key;
uncacheable_property = result.uncacheable_property;
}
// If cache_key is null, caching disabled
if (cache_key === null) {
// Set data-nocache attribute for debugging (shows which property prevented caching)
if (uncacheable_property) {
@@ -1574,8 +1592,25 @@ class Jqhtml_Component {
// Import coordinator and storage lazily to avoid circular dependency
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
// Generate cache key (same as deduplication key)
const { key: cache_key, uncacheable_property } = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
// Check if component implements cache_id() for custom cache key
let cache_key = null;
let uncacheable_property;
if (typeof this.cache_id === 'function') {
try {
const custom_cache_id = this.cache_id();
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
}
catch (error) {
// cache_id() threw error - disable caching
uncacheable_property = 'cache_id()';
}
}
else {
// Use standard args-based cache key generation
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
cache_key = result.key;
uncacheable_property = result.uncacheable_property;
}
// If cache_key is null, args are not serializable - skip load deduplication and caching
if (cache_key === null) {
// Set data-nocache attribute for debugging (shows which property prevented caching)
@@ -1916,7 +1951,23 @@ class Jqhtml_Component {
if (args_changed) {
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
const { key: cache_key, uncacheable_property } = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
// Check if component implements cache_id() for custom cache key
let cache_key = null;
if (typeof this.cache_id === 'function') {
try {
const custom_cache_id = this.cache_id();
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
}
catch (error) {
// cache_id() threw error - disable caching
cache_key = null;
}
}
else {
// Use standard args-based cache key generation
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
cache_key = result.key;
}
// Only use cache if args are serializable
if (cache_key !== null) {
const cached_data = Jqhtml_Local_Storage.get(cache_key);
@@ -1955,7 +2006,23 @@ class Jqhtml_Component {
if (data_changed && data_after_load !== '{}') {
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
const { key: cache_key, uncacheable_property } = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
// Check if component implements cache_id() for custom cache key
let cache_key = null;
if (typeof this.cache_id === 'function') {
try {
const custom_cache_id = this.cache_id();
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
}
catch (error) {
// cache_id() threw error - disable caching
cache_key = null;
}
}
else {
// Use standard args-based cache key generation
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
cache_key = result.key;
}
// Only update cache if args are serializable
if (cache_key !== null) {
Jqhtml_Local_Storage.set(cache_key, this.data);
@@ -2166,9 +2233,9 @@ class Jqhtml_Component {
* Searches for elements with id="local_id:THIS_COMPONENT_CID"
*
* Example:
* Template: <button $id="save_btn">Save</button>
* Rendered: <button id="save_btn:abc123" data-id="save_btn">Save</button>
* Access: this.$id('save_btn') // Returns jQuery element
* Template: <button $sid="save_btn">Save</button>
* Rendered: <button id="save_btn:abc123" data-sid="save_btn">Save</button>
* Access: this.$sid('save_btn') // Returns jQuery element
*
* Performance: Uses native document.getElementById() when component is in DOM,
* falls back to jQuery.find() for components not yet attached to DOM.
@@ -2176,7 +2243,7 @@ class Jqhtml_Component {
* @param local_id The local ID (without _cid suffix)
* @returns jQuery element with id="local_id:_cid", or empty jQuery object if not found
*/
$id(local_id) {
$sid(local_id) {
const scopedId = `${local_id}:${this._cid}`;
// Try getElementById first (fast path - works when component is in DOM)
const el = document.getElementById(scopedId);
@@ -2184,7 +2251,7 @@ class Jqhtml_Component {
return $(el);
}
// Fallback: component not in DOM yet, search within component subtree
// This allows $id() to work on components before they're appended to body
// This allows $sid() to work on components before they're appended to body
// Must escape the ID because it contains ':' which jQuery treats as a pseudo-selector
return this.$.find(`#${$.escapeSelector(scopedId)}`);
}
@@ -2194,19 +2261,22 @@ class Jqhtml_Component {
* Convenience method that finds element by scoped ID and returns the component instance.
*
* Example:
* Template: <User_Card $id="active_user" />
* Access: const user = this.id('active_user'); // Returns User_Card instance
* Template: <User_Card $sid="active_user" />
* Access: const user = this.sid('active_user'); // Returns User_Card instance
* user.data.name // Access component's data
*
* To get the scoped ID string itself:
* this.$sid('active_user').attr('id') // Returns "active_user:abc123xyz"
*
* @param local_id The local ID (without _cid suffix)
* @returns Component instance or null if not found or not a component
*/
id(local_id) {
const element = this.$id(local_id);
sid(local_id) {
const element = this.$sid(local_id);
const component = element.data('_component');
// If no component found but element exists, warn developer
if (!component && element.length > 0) {
console.warn(`Component ${this.constructor.name} tried to call .id('${local_id}') - ` +
console.warn(`Component ${this.constructor.name} tried to call .sid('${local_id}') - ` +
`${local_id} exists, however, it is not a component or $redrawable. ` +
`Did you forget to add $redrawable to the tag?`);
}
@@ -2825,7 +2895,7 @@ function evaluate_expression(expression, component, locals = {}) {
args: component.args,
$: component.$,
// Component methods
$id: component.$id.bind(component),
$sid: component.$sid.bind(component),
// Locals (like $event)
...locals
};
@@ -2853,7 +2923,7 @@ function evaluate_handler(expression, component) {
// Otherwise treat as inline code
try {
return new Function('$event', `
const { data, args, $, emit, $id } = this;
const { data, args, $, emit, $sid } = this;
${expression}
`).bind(component);
}
@@ -4151,7 +4221,7 @@ function init(jQuery) {
}
}
// Version - will be replaced during build with actual version from package.json
const version = '2.2.217';
const version = '2.2.218';
// Default export with all functionality
const jqhtml = {
// Core

2
node_modules/@jqhtml/core/dist/index.cjs.map generated vendored Executable file → Normal file

File diff suppressed because one or more lines are too long

0
node_modules/@jqhtml/core/dist/index.d.ts generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/core/dist/index.d.ts.map generated vendored Executable file → Normal file
View File

130
node_modules/@jqhtml/core/dist/index.js generated vendored Executable file → Normal file
View File

@@ -495,7 +495,7 @@ function process_tag_to_html(instruction, html, tagElements, components, context
if (key === 'id' && tid) {
// Special handling for id attribute - scope to parent component's _cid
// This is for regular id="foo" attributes that need scoping (rare case)
// Most scoping happens via $id attribute which becomes data-id
// Most scoping happens via $sid attribute which becomes data-sid
// Don't double-scope if already scoped (contains :)
if (typeof value === 'string' && value.includes(':')) {
html.push(` id="${value}"`);
@@ -545,13 +545,13 @@ function process_component_to_html(instruction, html, components, context) {
// Create element with tracking ID
html.push(`<${tagName} data-cid="${cid}"`);
// Handle id attributes for components
// The compiled code always generates both 'id' (scoped) and 'data-id' (base) for $id attributes
// The compiled code always generates both 'id' (scoped) and 'data-sid' (base) for $sid attributes
// We just pass through what the compiler gave us - NEVER regenerate
if (props['data-id']) {
const baseId = props['data-id'];
if (props['data-sid']) {
const baseId = props['data-sid'];
// The compiled code ALWAYS sets props['id'] with the correct scoped value
// Just use it directly - it already has the correct parent _cid baked in
html.push(` id="${props['id']}" data-id="${baseId}"`);
html.push(` id="${props['id']}" data-sid="${baseId}"`);
}
// Regular id passes through unchanged
else if (props['id']) {
@@ -1225,16 +1225,16 @@ class Jqhtml_Component {
// If id provided, delegate to child component
if (id) {
// First check if element with scoped ID exists
const $element = this.$id(id);
const $element = this.$sid(id);
if ($element.length === 0) {
throw new Error(`[JQHTML] render("${id}") - no such id.\n` +
`Component "${this.component_name()}" has no child element with $id="${id}".`);
`Component "${this.component_name()}" has no child element with $sid="${id}".`);
}
// Element exists, check if it's a component
const child = $element.data('_component');
if (!child) {
throw new Error(`[JQHTML] render("${id}") - element is not a component or does not have $redrawable attribute set.\n` +
`Element with $id="${id}" exists but is not initialized as a component.\n` +
`Element with $sid="${id}" exists but is not initialized as a component.\n` +
`Add $redrawable attribute or make it a proper component.`);
}
return child._render();
@@ -1455,15 +1455,15 @@ class Jqhtml_Component {
return;
// If id provided, delegate to child component
if (id) {
const $element = this.$id(id);
const $element = this.$sid(id);
if ($element.length === 0) {
throw new Error(`[JQHTML] render("${id}") - no such id.\n` +
`Component "${this.component_name()}" has no child element with $id="${id}".`);
`Component "${this.component_name()}" has no child element with $sid="${id}".`);
}
const child = $element.data('_component');
if (!child) {
throw new Error(`[JQHTML] render("${id}") - element is not a component or does not have $redrawable attribute set.\n` +
`Element with $id="${id}" exists but is not initialized as a component.\n` +
`Element with $sid="${id}" exists but is not initialized as a component.\n` +
`Add $redrawable attribute or make it a proper component.`);
}
return child.render();
@@ -1511,8 +1511,26 @@ class Jqhtml_Component {
// This happens after on_create() but before render, allowing instant first render with cached data
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
const { key: cache_key, uncacheable_property } = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
// If cache_key is null, args are not serializable - skip caching
// Check if component implements cache_id() for custom cache key
let cache_key = null;
let uncacheable_property;
if (typeof this.cache_id === 'function') {
try {
const custom_cache_id = this.cache_id();
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
}
catch (error) {
// cache_id() threw error - disable caching
uncacheable_property = 'cache_id()';
}
}
else {
// Use standard args-based cache key generation
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
cache_key = result.key;
uncacheable_property = result.uncacheable_property;
}
// If cache_key is null, caching disabled
if (cache_key === null) {
// Set data-nocache attribute for debugging (shows which property prevented caching)
if (uncacheable_property) {
@@ -1570,8 +1588,25 @@ class Jqhtml_Component {
// Import coordinator and storage lazily to avoid circular dependency
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
// Generate cache key (same as deduplication key)
const { key: cache_key, uncacheable_property } = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
// Check if component implements cache_id() for custom cache key
let cache_key = null;
let uncacheable_property;
if (typeof this.cache_id === 'function') {
try {
const custom_cache_id = this.cache_id();
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
}
catch (error) {
// cache_id() threw error - disable caching
uncacheable_property = 'cache_id()';
}
}
else {
// Use standard args-based cache key generation
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
cache_key = result.key;
uncacheable_property = result.uncacheable_property;
}
// If cache_key is null, args are not serializable - skip load deduplication and caching
if (cache_key === null) {
// Set data-nocache attribute for debugging (shows which property prevented caching)
@@ -1912,7 +1947,23 @@ class Jqhtml_Component {
if (args_changed) {
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
const { key: cache_key, uncacheable_property } = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
// Check if component implements cache_id() for custom cache key
let cache_key = null;
if (typeof this.cache_id === 'function') {
try {
const custom_cache_id = this.cache_id();
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
}
catch (error) {
// cache_id() threw error - disable caching
cache_key = null;
}
}
else {
// Use standard args-based cache key generation
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
cache_key = result.key;
}
// Only use cache if args are serializable
if (cache_key !== null) {
const cached_data = Jqhtml_Local_Storage.get(cache_key);
@@ -1951,7 +2002,23 @@ class Jqhtml_Component {
if (data_changed && data_after_load !== '{}') {
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
const { key: cache_key, uncacheable_property } = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
// Check if component implements cache_id() for custom cache key
let cache_key = null;
if (typeof this.cache_id === 'function') {
try {
const custom_cache_id = this.cache_id();
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
}
catch (error) {
// cache_id() threw error - disable caching
cache_key = null;
}
}
else {
// Use standard args-based cache key generation
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
cache_key = result.key;
}
// Only update cache if args are serializable
if (cache_key !== null) {
Jqhtml_Local_Storage.set(cache_key, this.data);
@@ -2162,9 +2229,9 @@ class Jqhtml_Component {
* Searches for elements with id="local_id:THIS_COMPONENT_CID"
*
* Example:
* Template: <button $id="save_btn">Save</button>
* Rendered: <button id="save_btn:abc123" data-id="save_btn">Save</button>
* Access: this.$id('save_btn') // Returns jQuery element
* Template: <button $sid="save_btn">Save</button>
* Rendered: <button id="save_btn:abc123" data-sid="save_btn">Save</button>
* Access: this.$sid('save_btn') // Returns jQuery element
*
* Performance: Uses native document.getElementById() when component is in DOM,
* falls back to jQuery.find() for components not yet attached to DOM.
@@ -2172,7 +2239,7 @@ class Jqhtml_Component {
* @param local_id The local ID (without _cid suffix)
* @returns jQuery element with id="local_id:_cid", or empty jQuery object if not found
*/
$id(local_id) {
$sid(local_id) {
const scopedId = `${local_id}:${this._cid}`;
// Try getElementById first (fast path - works when component is in DOM)
const el = document.getElementById(scopedId);
@@ -2180,7 +2247,7 @@ class Jqhtml_Component {
return $(el);
}
// Fallback: component not in DOM yet, search within component subtree
// This allows $id() to work on components before they're appended to body
// This allows $sid() to work on components before they're appended to body
// Must escape the ID because it contains ':' which jQuery treats as a pseudo-selector
return this.$.find(`#${$.escapeSelector(scopedId)}`);
}
@@ -2190,19 +2257,22 @@ class Jqhtml_Component {
* Convenience method that finds element by scoped ID and returns the component instance.
*
* Example:
* Template: <User_Card $id="active_user" />
* Access: const user = this.id('active_user'); // Returns User_Card instance
* Template: <User_Card $sid="active_user" />
* Access: const user = this.sid('active_user'); // Returns User_Card instance
* user.data.name // Access component's data
*
* To get the scoped ID string itself:
* this.$sid('active_user').attr('id') // Returns "active_user:abc123xyz"
*
* @param local_id The local ID (without _cid suffix)
* @returns Component instance or null if not found or not a component
*/
id(local_id) {
const element = this.$id(local_id);
sid(local_id) {
const element = this.$sid(local_id);
const component = element.data('_component');
// If no component found but element exists, warn developer
if (!component && element.length > 0) {
console.warn(`Component ${this.constructor.name} tried to call .id('${local_id}') - ` +
console.warn(`Component ${this.constructor.name} tried to call .sid('${local_id}') - ` +
`${local_id} exists, however, it is not a component or $redrawable. ` +
`Did you forget to add $redrawable to the tag?`);
}
@@ -2821,7 +2891,7 @@ function evaluate_expression(expression, component, locals = {}) {
args: component.args,
$: component.$,
// Component methods
$id: component.$id.bind(component),
$sid: component.$sid.bind(component),
// Locals (like $event)
...locals
};
@@ -2849,7 +2919,7 @@ function evaluate_handler(expression, component) {
// Otherwise treat as inline code
try {
return new Function('$event', `
const { data, args, $, emit, $id } = this;
const { data, args, $, emit, $sid } = this;
${expression}
`).bind(component);
}
@@ -4147,7 +4217,7 @@ function init(jQuery) {
}
}
// Version - will be replaced during build with actual version from package.json
const version = '2.2.217';
const version = '2.2.218';
// Default export with all functionality
const jqhtml = {
// Core

2
node_modules/@jqhtml/core/dist/index.js.map generated vendored Executable file → Normal file

File diff suppressed because one or more lines are too long

0
node_modules/@jqhtml/core/dist/instruction-processor.d.ts generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/core/dist/instruction-processor.d.ts.map generated vendored Executable file → Normal file
View File

132
node_modules/@jqhtml/core/dist/jqhtml-core.esm.js generated vendored Executable file → Normal file
View File

@@ -1,5 +1,5 @@
/**
* JQHTML Core v2.2.217
* JQHTML Core v2.2.218
* (c) 2025 JQHTML Team
* Released under the MIT License
*/
@@ -500,7 +500,7 @@ function process_tag_to_html(instruction, html, tagElements, components, context
if (key === 'id' && tid) {
// Special handling for id attribute - scope to parent component's _cid
// This is for regular id="foo" attributes that need scoping (rare case)
// Most scoping happens via $id attribute which becomes data-id
// Most scoping happens via $sid attribute which becomes data-sid
// Don't double-scope if already scoped (contains :)
if (typeof value === 'string' && value.includes(':')) {
html.push(` id="${value}"`);
@@ -550,13 +550,13 @@ function process_component_to_html(instruction, html, components, context) {
// Create element with tracking ID
html.push(`<${tagName} data-cid="${cid}"`);
// Handle id attributes for components
// The compiled code always generates both 'id' (scoped) and 'data-id' (base) for $id attributes
// The compiled code always generates both 'id' (scoped) and 'data-sid' (base) for $sid attributes
// We just pass through what the compiler gave us - NEVER regenerate
if (props['data-id']) {
const baseId = props['data-id'];
if (props['data-sid']) {
const baseId = props['data-sid'];
// The compiled code ALWAYS sets props['id'] with the correct scoped value
// Just use it directly - it already has the correct parent _cid baked in
html.push(` id="${props['id']}" data-id="${baseId}"`);
html.push(` id="${props['id']}" data-sid="${baseId}"`);
}
// Regular id passes through unchanged
else if (props['id']) {
@@ -1230,16 +1230,16 @@ class Jqhtml_Component {
// If id provided, delegate to child component
if (id) {
// First check if element with scoped ID exists
const $element = this.$id(id);
const $element = this.$sid(id);
if ($element.length === 0) {
throw new Error(`[JQHTML] render("${id}") - no such id.\n` +
`Component "${this.component_name()}" has no child element with $id="${id}".`);
`Component "${this.component_name()}" has no child element with $sid="${id}".`);
}
// Element exists, check if it's a component
const child = $element.data('_component');
if (!child) {
throw new Error(`[JQHTML] render("${id}") - element is not a component or does not have $redrawable attribute set.\n` +
`Element with $id="${id}" exists but is not initialized as a component.\n` +
`Element with $sid="${id}" exists but is not initialized as a component.\n` +
`Add $redrawable attribute or make it a proper component.`);
}
return child._render();
@@ -1460,15 +1460,15 @@ class Jqhtml_Component {
return;
// If id provided, delegate to child component
if (id) {
const $element = this.$id(id);
const $element = this.$sid(id);
if ($element.length === 0) {
throw new Error(`[JQHTML] render("${id}") - no such id.\n` +
`Component "${this.component_name()}" has no child element with $id="${id}".`);
`Component "${this.component_name()}" has no child element with $sid="${id}".`);
}
const child = $element.data('_component');
if (!child) {
throw new Error(`[JQHTML] render("${id}") - element is not a component or does not have $redrawable attribute set.\n` +
`Element with $id="${id}" exists but is not initialized as a component.\n` +
`Element with $sid="${id}" exists but is not initialized as a component.\n` +
`Add $redrawable attribute or make it a proper component.`);
}
return child.render();
@@ -1516,8 +1516,26 @@ class Jqhtml_Component {
// This happens after on_create() but before render, allowing instant first render with cached data
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
const { key: cache_key, uncacheable_property } = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
// If cache_key is null, args are not serializable - skip caching
// Check if component implements cache_id() for custom cache key
let cache_key = null;
let uncacheable_property;
if (typeof this.cache_id === 'function') {
try {
const custom_cache_id = this.cache_id();
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
}
catch (error) {
// cache_id() threw error - disable caching
uncacheable_property = 'cache_id()';
}
}
else {
// Use standard args-based cache key generation
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
cache_key = result.key;
uncacheable_property = result.uncacheable_property;
}
// If cache_key is null, caching disabled
if (cache_key === null) {
// Set data-nocache attribute for debugging (shows which property prevented caching)
if (uncacheable_property) {
@@ -1575,8 +1593,25 @@ class Jqhtml_Component {
// Import coordinator and storage lazily to avoid circular dependency
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
// Generate cache key (same as deduplication key)
const { key: cache_key, uncacheable_property } = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
// Check if component implements cache_id() for custom cache key
let cache_key = null;
let uncacheable_property;
if (typeof this.cache_id === 'function') {
try {
const custom_cache_id = this.cache_id();
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
}
catch (error) {
// cache_id() threw error - disable caching
uncacheable_property = 'cache_id()';
}
}
else {
// Use standard args-based cache key generation
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
cache_key = result.key;
uncacheable_property = result.uncacheable_property;
}
// If cache_key is null, args are not serializable - skip load deduplication and caching
if (cache_key === null) {
// Set data-nocache attribute for debugging (shows which property prevented caching)
@@ -1917,7 +1952,23 @@ class Jqhtml_Component {
if (args_changed) {
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
const { key: cache_key, uncacheable_property } = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
// Check if component implements cache_id() for custom cache key
let cache_key = null;
if (typeof this.cache_id === 'function') {
try {
const custom_cache_id = this.cache_id();
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
}
catch (error) {
// cache_id() threw error - disable caching
cache_key = null;
}
}
else {
// Use standard args-based cache key generation
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
cache_key = result.key;
}
// Only use cache if args are serializable
if (cache_key !== null) {
const cached_data = Jqhtml_Local_Storage.get(cache_key);
@@ -1956,7 +2007,23 @@ class Jqhtml_Component {
if (data_changed && data_after_load !== '{}') {
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
const { key: cache_key, uncacheable_property } = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
// Check if component implements cache_id() for custom cache key
let cache_key = null;
if (typeof this.cache_id === 'function') {
try {
const custom_cache_id = this.cache_id();
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
}
catch (error) {
// cache_id() threw error - disable caching
cache_key = null;
}
}
else {
// Use standard args-based cache key generation
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
cache_key = result.key;
}
// Only update cache if args are serializable
if (cache_key !== null) {
Jqhtml_Local_Storage.set(cache_key, this.data);
@@ -2167,9 +2234,9 @@ class Jqhtml_Component {
* Searches for elements with id="local_id:THIS_COMPONENT_CID"
*
* Example:
* Template: <button $id="save_btn">Save</button>
* Rendered: <button id="save_btn:abc123" data-id="save_btn">Save</button>
* Access: this.$id('save_btn') // Returns jQuery element
* Template: <button $sid="save_btn">Save</button>
* Rendered: <button id="save_btn:abc123" data-sid="save_btn">Save</button>
* Access: this.$sid('save_btn') // Returns jQuery element
*
* Performance: Uses native document.getElementById() when component is in DOM,
* falls back to jQuery.find() for components not yet attached to DOM.
@@ -2177,7 +2244,7 @@ class Jqhtml_Component {
* @param local_id The local ID (without _cid suffix)
* @returns jQuery element with id="local_id:_cid", or empty jQuery object if not found
*/
$id(local_id) {
$sid(local_id) {
const scopedId = `${local_id}:${this._cid}`;
// Try getElementById first (fast path - works when component is in DOM)
const el = document.getElementById(scopedId);
@@ -2185,7 +2252,7 @@ class Jqhtml_Component {
return $(el);
}
// Fallback: component not in DOM yet, search within component subtree
// This allows $id() to work on components before they're appended to body
// This allows $sid() to work on components before they're appended to body
// Must escape the ID because it contains ':' which jQuery treats as a pseudo-selector
return this.$.find(`#${$.escapeSelector(scopedId)}`);
}
@@ -2195,19 +2262,22 @@ class Jqhtml_Component {
* Convenience method that finds element by scoped ID and returns the component instance.
*
* Example:
* Template: <User_Card $id="active_user" />
* Access: const user = this.id('active_user'); // Returns User_Card instance
* Template: <User_Card $sid="active_user" />
* Access: const user = this.sid('active_user'); // Returns User_Card instance
* user.data.name // Access component's data
*
* To get the scoped ID string itself:
* this.$sid('active_user').attr('id') // Returns "active_user:abc123xyz"
*
* @param local_id The local ID (without _cid suffix)
* @returns Component instance or null if not found or not a component
*/
id(local_id) {
const element = this.$id(local_id);
sid(local_id) {
const element = this.$sid(local_id);
const component = element.data('_component');
// If no component found but element exists, warn developer
if (!component && element.length > 0) {
console.warn(`Component ${this.constructor.name} tried to call .id('${local_id}') - ` +
console.warn(`Component ${this.constructor.name} tried to call .sid('${local_id}') - ` +
`${local_id} exists, however, it is not a component or $redrawable. ` +
`Did you forget to add $redrawable to the tag?`);
}
@@ -2826,7 +2896,7 @@ function evaluate_expression(expression, component, locals = {}) {
args: component.args,
$: component.$,
// Component methods
$id: component.$id.bind(component),
$sid: component.$sid.bind(component),
// Locals (like $event)
...locals
};
@@ -2854,7 +2924,7 @@ function evaluate_handler(expression, component) {
// Otherwise treat as inline code
try {
return new Function('$event', `
const { data, args, $, emit, $id } = this;
const { data, args, $, emit, $sid } = this;
${expression}
`).bind(component);
}
@@ -4152,7 +4222,7 @@ function init(jQuery) {
}
}
// Version - will be replaced during build with actual version from package.json
const version = '2.2.217';
const version = '2.2.218';
// Default export with all functionality
const jqhtml = {
// Core

2
node_modules/@jqhtml/core/dist/jqhtml-core.esm.js.map generated vendored Executable file → Normal file

File diff suppressed because one or more lines are too long

0
node_modules/@jqhtml/core/dist/jqhtml-debug.esm.js generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/core/dist/jqhtml-debug.esm.js.map generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/core/dist/jquery-plugin.d.ts generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/core/dist/jquery-plugin.d.ts.map generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/core/dist/lifecycle-manager.d.ts generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/core/dist/lifecycle-manager.d.ts.map generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/core/dist/load-coordinator.d.ts generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/core/dist/load-coordinator.d.ts.map generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/core/dist/local-storage.d.ts generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/core/dist/local-storage.d.ts.map generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/core/dist/template-renderer.d.ts generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/core/dist/template-renderer.d.ts.map generated vendored Executable file → Normal file
View File

2
node_modules/@jqhtml/core/package.json generated vendored Executable file → Normal file
View File

@@ -1,6 +1,6 @@
{
"name": "@jqhtml/core",
"version": "2.2.217",
"version": "2.2.218",
"description": "Core runtime library for JQHTML",
"type": "module",
"main": "./dist/index.js",

View File

@@ -177,15 +177,15 @@ JQHTML uses the `$` prefix as a shorthand for data attributes with special handl
2. For debugging visibility, if the value is a string or number, it's also set as a DOM attribute
3. Objects and arrays are stored in `.data()` but not visible in DOM
#### Special Case: `$id="name"` → Scoped IDs
#### Special Case: `$sid="name"` → Scoped IDs
The `$id` attribute has special handling for component-scoped element selection:
The `$sid` attribute has special handling for component-scoped element selection:
```jqhtml
<Define:UserCard>
<div $id="container">
<input $id="username" type="text" />
<button $id="submit">Submit</button>
<div $sid="container">
<input $sid="username" type="text" />
<button $sid="submit">Submit</button>
</div>
</Define:UserCard>
```
@@ -207,8 +207,8 @@ function render(_cid) {
class UserCard extends Component {
init() {
// Find scoped elements
const $username = this.$id('username'); // Returns $('#username:123')
const $submit = this.$id('submit'); // Returns $('#submit:123')
const $username = this.$sid('username'); // Returns $('#username:123')
const $submit = this.$sid('submit'); // Returns $('#submit:123')
$submit.on('click', () => {
const value = $username.val();
@@ -224,15 +224,15 @@ Component IDs flow through lexical scope naturally:
```jqhtml
<Define:ParentComponent>
<div $id="parent-element"> <!-- Gets ParentComponent's _cid -->
<div $sid="parent-element"> <!-- Gets ParentComponent's _cid -->
<ChildComponent>
<div $id="slot-element" /> <!-- Also gets ParentComponent's _cid -->
<div $sid="slot-element" /> <!-- Also gets ParentComponent's _cid -->
</ChildComponent>
</div>
</Define:ParentComponent>
<Define:ChildComponent>
<div $id="child-element"> <!-- Gets ChildComponent's _cid -->
<div $sid="child-element"> <!-- Gets ChildComponent's _cid -->
<%= content() %> <!-- Slot content preserves parent's _cid -->
</div>
</Define:ChildComponent>
@@ -374,9 +374,9 @@ The `@` prefix binds event handlers:
<!-- Wrapper that adds behavior to innerHTML -->
<Define:Collapsible>
<div class="collapsible" $id="wrapper">
<div class="collapsible" $sid="wrapper">
<button $onclick="toggle">Toggle</button>
<div class="content" $id="content">
<div class="content" $sid="content">
<%= content() %> <!-- Wrapped innerHTML -->
</div>
</div>
@@ -481,7 +481,7 @@ Templates compile to three instruction types:
{tag: ["div", {"class": "user-card"}, false]}
{tag: ["img", {"src": "/avatar.jpg", "alt": "User"}, true]}
{tag: ["button", {"onclick": this.handleClick}, false]}
{tag: ["div", {"data-id": "header"}, false]} // $id becomes data-id
{tag: ["div", {"data-sid": "header"}, false]} // $id becomes data-id
```
### 2. Component Instruction

View File

@@ -390,7 +390,7 @@ These patterns inform v2 implementation decisions, particularly around maintaini
- **Expressions**: `<%= expression %>` for output
- **Bindings**: `:text`, `:value`, `:class`, `:style` for data binding
- **Events**: `@click`, `@change`, etc. for event handlers
- **Scoped IDs**: `$id` attribute for component-scoped IDs
- **Scoped IDs**: `$sid` attribute for component-scoped IDs
### Instruction Format

0
node_modules/@jqhtml/parser/dist/ast.d.ts generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/parser/dist/ast.d.ts.map generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/parser/dist/ast.js generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/parser/dist/ast.js.map generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/parser/dist/codegen.d.ts generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/parser/dist/codegen.d.ts.map generated vendored Executable file → Normal file
View File

22
node_modules/@jqhtml/parser/dist/codegen.js generated vendored Executable file → Normal file
View File

@@ -1042,14 +1042,14 @@ export class CodeGenerator {
const entries = Object.entries(attrs).flatMap(([key, value]) => {
// Convert 'tag' to '_tag' for component invocations
const attrKey = key === 'tag' ? '_tag' : key;
// Special handling for data-id attribute (from $id) - create scoped id
// NOTE: Parser converts $id="foo" → data-id="foo" so we can distinguish from regular id
// This generates: id="foo:PARENT_CID" data-id="foo"
// Special handling for data-sid attribute (from $sid) - create scoped id
// NOTE: Parser converts $sid="foo" → data-sid="foo" so we can distinguish from regular id
// This generates: id="foo:PARENT_CID" data-sid="foo"
// The :PARENT_CID scoping happens at runtime in instruction-processor.ts
if (key === 'data-id') {
if (key === 'data-sid') {
const id_entries = [];
if (value && typeof value === 'object' && value.interpolated) {
// Interpolated $id like $id="user<%= index %>"
// Interpolated $sid like $sid="user<%= index %>"
const parts = value.parts.map((part) => {
if (part.type === 'text') {
return this.escape_string(part.value);
@@ -1060,19 +1060,19 @@ export class CodeGenerator {
});
const base_id = parts.join(' + ');
id_entries.push(`"id": ${base_id} + ":" + this._cid`);
id_entries.push(`"data-id": ${base_id}`);
id_entries.push(`"data-sid": ${base_id}`);
}
else if (value && typeof value === 'object' && value.quoted) {
// Quoted $id like $id="static"
// Quoted $sid like $sid="static"
const base_id = this.escape_string(value.value);
id_entries.push(`"id": ${base_id} + ":" + this._cid`);
id_entries.push(`"data-id": ${base_id}`);
id_entries.push(`"data-sid": ${base_id}`);
}
else {
// Simple $id like $id="username" or expression like $id=someVar
// Simple $sid like $sid="username" or expression like $sid=someVar
const base_id = this.escape_string(String(value));
id_entries.push(`"id": ${base_id} + ":" + this._cid`);
id_entries.push(`"data-id": ${base_id}`);
id_entries.push(`"data-sid": ${base_id}`);
}
return id_entries;
}
@@ -1348,7 +1348,7 @@ export class CodeGenerator {
for (const [name, component] of this.components) {
code += `// Component: ${name}\n`;
code += `jqhtml_components.set('${name}', {\n`;
code += ` _jqhtml_version: '2.2.217',\n`; // Version will be replaced during build
code += ` _jqhtml_version: '2.2.218',\n`; // Version will be replaced during build
code += ` name: '${name}',\n`;
code += ` tag: '${component.tagName}',\n`;
code += ` defaultAttributes: ${this.serializeAttributeObject(component.defaultAttributes)},\n`;

2
node_modules/@jqhtml/parser/dist/codegen.js.map generated vendored Executable file → Normal file

File diff suppressed because one or more lines are too long

0
node_modules/@jqhtml/parser/dist/compiler.d.ts generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/parser/dist/compiler.d.ts.map generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/parser/dist/compiler.js generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/parser/dist/compiler.js.map generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/parser/dist/errors.d.ts generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/parser/dist/errors.d.ts.map generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/parser/dist/errors.js generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/parser/dist/errors.js.map generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/parser/dist/index.d.ts generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/parser/dist/index.d.ts.map generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/parser/dist/index.js generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/parser/dist/index.js.map generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/parser/dist/integration.d.ts generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/parser/dist/integration.d.ts.map generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/parser/dist/integration.js generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/parser/dist/integration.js.map generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/parser/dist/lexer.d.ts generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/parser/dist/lexer.d.ts.map generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/parser/dist/lexer.js generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/parser/dist/lexer.js.map generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/parser/dist/parser.d.ts generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/parser/dist/parser.d.ts.map generated vendored Executable file → Normal file
View File

14
node_modules/@jqhtml/parser/dist/parser.js generated vendored Executable file → Normal file
View File

@@ -109,9 +109,9 @@ export class Parser {
}
while (!this.check(TokenType.GT) && !this.is_at_end()) {
const attr_name = this.consume(TokenType.ATTR_NAME, 'Expected attribute name');
// Validate that $id is not used in Define tags
if (attr_name.value === '$id') {
throw syntaxError('$id is not allowed in <Define:> tags. Component definitions cannot have scoped IDs.', attr_name.line, attr_name.column, this.source, this.filename);
// Validate that $sid is not used in Define tags
if (attr_name.value === '$sid') {
throw syntaxError('$sid is not allowed in <Define:> tags. Component definitions cannot have scoped IDs.', attr_name.line, attr_name.column, this.source, this.filename);
}
this.consume(TokenType.EQUALS, 'Expected =');
const attr_value = this.parse_attribute_value();
@@ -535,10 +535,10 @@ export class Parser {
}
// Handle special attribute prefixes
if (name.startsWith('$')) {
// Special case: $id becomes data-id (needed for scoped ID system)
// Special case: $sid becomes data-sid (needed for scoped ID system)
// All other $ attributes stay as-is (handled by instruction-processor.ts)
if (name === '$id') {
name = 'data-id';
if (name === '$sid') {
name = 'data-sid';
}
// Keep $ prefix for other attributes - they get stored via .data() at runtime
// Keep the value object intact to preserve quoted/unquoted distinction
@@ -781,7 +781,7 @@ export class Parser {
' Or set attributes in on_ready() using jQuery:\n' +
' ✅ <tag $id="my_element">\n' +
' on_ready() {\n' +
' if (this.args.required) this.$id(\'my_element\').attr(\'required\', true);\n' +
' if (this.args.required) this.$sid(\'my_element\').attr(\'required\', true);\n' +
' }';
throw error;
}

2
node_modules/@jqhtml/parser/dist/parser.js.map generated vendored Executable file → Normal file

File diff suppressed because one or more lines are too long

0
node_modules/@jqhtml/parser/dist/runtime.d.ts generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/parser/dist/runtime.d.ts.map generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/parser/dist/runtime.js generated vendored Executable file → Normal file
View File

0
node_modules/@jqhtml/parser/dist/runtime.js.map generated vendored Executable file → Normal file
View File

Some files were not shown because too many files have changed in this diff Show More