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); $display_name = str_replace('_Component', '', $class_name);
return <<<JQHTML return <<<JQHTML
<!-- <%--
{$class_name} {$class_name}
\$button_text="Click Me" - Text shown on the button \$button_text="Click Me" - Text shown on the button
--> --%>
<Define:{$class_name} style="border: 1px solid black;"> <Define:{$class_name} style="border: 1px solid black;">
<h4>{$display_name}</h4> <h4>{$display_name}</h4>
<div \$id="hello_world" style="font-weight: bold; display: none;"> <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 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 * Flag to indicate AJAX response mode for error handlers
*/ */
protected static bool $ajax_response_mode = false; 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 * Call an internal API method directly from PHP code
* *
@@ -157,19 +188,26 @@ class Ajax
$details = $response->get_details(); $details = $response->get_details();
switch ($type) { switch ($type) {
case 'response_auth_required': case self::ERROR_AUTH_REQUIRED:
throw new AjaxAuthRequiredException($reason); throw new AjaxAuthRequiredException($reason);
case 'response_unauthorized':
case self::ERROR_UNAUTHORIZED:
throw new AjaxUnauthorizedException($reason); throw new AjaxUnauthorizedException($reason);
case 'response_form_error':
case self::ERROR_VALIDATION:
case self::ERROR_NOT_FOUND:
throw new AjaxFormErrorException($reason, $details); throw new AjaxFormErrorException($reason, $details);
case 'fatal':
case self::ERROR_FATAL:
$message = $reason; $message = $reason;
if (!empty($details)) { if (!empty($details)) {
$message .= ' - ' . json_encode($details); $message .= ' - ' . json_encode($details);
} }
throw new AjaxFatalErrorException($message); throw new AjaxFatalErrorException($message);
case self::ERROR_GENERIC:
throw new Exception($reason);
default: default:
throw new Exception("Unknown RSX response type: {$type}"); throw new Exception("Unknown RSX response type: {$type}");
} }
@@ -324,18 +362,14 @@ class Ajax
throw new Exception($message); throw new Exception($message);
} }
// Build error response based on type // Build error response
$json_response = [ $json_response = [
'_success' => false, '_success' => false,
'error_type' => $type, 'error_code' => $response->get_error_code(),
'reason' => $response->get_reason(), '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 // Add console debug messages if any
$console_messages = \App\RSpade\Core\Debug\Debugger::_get_console_messages(); $console_messages = \App\RSpade\Core\Debug\Debugger::_get_console_messages();
if (!empty($console_messages)) { if (!empty($console_messages)) {
@@ -395,7 +429,7 @@ class Ajax
if ((array_key_exists('_success', $response) && is_bool($response['_success'])) || if ((array_key_exists('_success', $response) && is_bool($response['_success'])) ||
(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'];"; $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_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"; $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) { } catch (Exceptions\AjaxAuthRequiredException $e) {
$responses["C_{$call_id}"] = [ $responses["C_{$call_id}"] = [
'success' => false, 'success' => false,
'error_type' => 'response_auth_required', 'error_code' => Ajax::ERROR_AUTH_REQUIRED,
'reason' => $e->getMessage(), 'reason' => $e->getMessage(),
'metadata' => [],
]; ];
} catch (Exceptions\AjaxUnauthorizedException $e) { } catch (Exceptions\AjaxUnauthorizedException $e) {
$responses["C_{$call_id}"] = [ $responses["C_{$call_id}"] = [
'success' => false, 'success' => false,
'error_type' => 'response_unauthorized', 'error_code' => Ajax::ERROR_UNAUTHORIZED,
'reason' => $e->getMessage(), 'reason' => $e->getMessage(),
'metadata' => [],
]; ];
} catch (Exceptions\AjaxFormErrorException $e) { } catch (Exceptions\AjaxFormErrorException $e) {
$responses["C_{$call_id}"] = [ $responses["C_{$call_id}"] = [
'success' => false, 'success' => false,
'error_type' => 'response_form_error', 'error_code' => Ajax::ERROR_VALIDATION,
'reason' => $e->getMessage(), 'reason' => $e->getMessage(),
'details' => $e->get_details(), 'metadata' => $e->get_details(),
]; ];
} catch (Exceptions\AjaxFatalErrorException $e) { } catch (Exceptions\AjaxFatalErrorException $e) {
$responses["C_{$call_id}"] = [ $responses["C_{$call_id}"] = [
'success' => false, 'success' => false,
'error_type' => 'fatal', 'error_code' => Ajax::ERROR_FATAL,
'reason' => $e->getMessage(), 'reason' => $e->getMessage(),
'metadata' => [],
]; ];
} catch (\Exception $e) { } catch (\Exception $e) {

View File

@@ -2176,16 +2176,20 @@ JS;
foreach ($file_info['public_static_methods'] ?? [] as $method_name => $method_data) { foreach ($file_info['public_static_methods'] ?? [] as $method_name => $method_data) {
foreach ($method_data['attributes'] ?? [] as $attr_name => $attr_instances) { foreach ($method_data['attributes'] ?? [] as $attr_name => $attr_instances) {
if ($attr_name === 'Route') { 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) { foreach ($attr_instances as $instance) {
$route_pattern = $instance[0] ?? null; $route_pattern = $instance[0] ?? null;
if ($route_pattern) { if ($route_pattern) {
console_debug('BUNDLE', " Found route: {$class_name}::{$method_name} => {$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])) { if (!isset($routes[$class_name])) {
$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 ($file_info['public_static_methods'] ?? [] as $method_name => $method_data) {
foreach ($method_data['attributes'] ?? [] as $attr_name => $attr_instances) { foreach ($method_data['attributes'] ?? [] as $attr_name => $attr_instances) {
if ($attr_name === 'Route') { 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) { foreach ($attr_instances as $instance) {
$route_pattern = $instance[0] ?? null; $route_pattern = $instance[0] ?? null;
if ($route_pattern) { if ($route_pattern) {
// Store route info // Initialize arrays if needed
if (!isset($routes[$class_name])) { if (!isset($routes[$class_name])) {
$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 * Returns array of {value: country_code, label: country_name} sorted alphabetically
*/ */
#[Ajax_Endpoint] #[Ajax_Endpoint]
public static function countries(Request $request, array $params = []): array public static function countries(Request $request, array $params = [])
{ {
return Country_Model::enabled() return Country_Model::enabled()
->orderBy('name') ->orderBy('name')
@@ -41,10 +41,9 @@ class Rsx_Reference_Data_Controller extends Rsx_Controller_Abstract
* *
* @param Request $request * @param Request $request
* @param array $params - Expected: ['country' => 'US'] * @param array $params - Expected: ['country' => 'US']
* @return array
*/ */
#[Ajax_Endpoint] #[Ajax_Endpoint]
public static function states(Request $request, array $params = []): array public static function states(Request $request, array $params = [])
{ {
$country = $params['country'] ?? 'US'; $country = $params['country'] ?? 'US';

View File

@@ -19,10 +19,9 @@ class Debugger_Controller extends Rsx_Controller_Abstract
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @param array $params * @param array $params
* @return array
*/ */
#[Ajax_Endpoint] #[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); return Debugger::log_console_messages($request, $params);
} }
@@ -32,10 +31,9 @@ class Debugger_Controller extends Rsx_Controller_Abstract
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @param array $params * @param array $params
* @return array
*/ */
#[Ajax_Endpoint] #[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); return Debugger::log_browser_errors($request, $params);
} }

View File

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

View File

@@ -7,6 +7,16 @@
* Batches up to 20 calls or flushes after setTimeout(0) debounce. * Batches up to 20 calls or flushes after setTimeout(0) debounce.
*/ */
class Ajax { 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 * Initialize Ajax system
* Called automatically when class is loaded * Called automatically when class is loaded
@@ -198,81 +208,70 @@ class Ajax {
resolve(processed_value); resolve(processed_value);
} else { } else {
// Handle error responses // Handle error responses
const error_type = response.error_type || 'unknown_error'; const error_code = response.error_code || Ajax.ERROR_GENERIC;
const reason = response.reason || 'Unknown error occurred'; const reason = response.reason || 'An error occurred';
const details = response.details || {}; const metadata = response.metadata || {};
// Handle specific error types // Create error object
switch (error_type) { const error = new Error(reason);
case 'fatal': error.code = error_code;
// Fatal PHP error with full error details error.metadata = metadata;
const fatal_error_data = response.error || {};
const error_message = fatal_error_data.error || 'Fatal error occurred';
console.error('Ajax error response from server:', response.error); // Handle fatal errors specially
if (error_code === Ajax.ERROR_FATAL) {
const fatal_error_data = response.error || {};
error.message = fatal_error_data.error || 'Fatal error occurred';
error.metadata = response.error;
const fatal_error = new Error(error_message); console.error('Ajax error response from server:', response.error);
fatal_error.type = 'fatal';
fatal_error.details = response.error;
// Log to server if browser error logging is enabled // Log to server
Debugger.log_error({ Debugger.log_error({
message: `Ajax Fatal Error: ${error_message}`, message: `Ajax Fatal Error: ${error.message}`,
type: 'ajax_fatal', type: 'ajax_fatal',
endpoint: url, endpoint: url,
details: response.error, 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) => { error: (xhr, status, error) => {
const error_message = Ajax._extract_error_message(xhr); const err = new Error();
const network_error = new Error(error_message);
network_error.type = 'network_error';
network_error.status = xhr.status;
network_error.statusText = status;
// 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) { if (xhr.status >= 500) {
Debugger.log_error({ Debugger.log_error({
message: `Ajax Server Error ${xhr.status}: ${error_message}`, message: `Ajax Server Error ${xhr.status}: ${err.message}`,
type: 'ajax_server_error', type: 'ajax_server_error',
endpoint: url, endpoint: url,
status: xhr.status, status: xhr.status,
@@ -280,7 +279,7 @@ class Ajax {
}); });
} }
reject(network_error); reject(err);
}, },
}); });
}); });
@@ -376,26 +375,13 @@ class Ajax {
}); });
} else { } else {
// Handle error // Handle error
const error_type = call_response.error_type || 'unknown_error'; const error_code = call_response.error_code || Ajax.ERROR_GENERIC;
let error_message; const error_message = call_response.reason || 'Unknown error occurred';
let error_details; const metadata = call_response.metadata || {};
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 = new Error(error_message); const error = new Error(error_message);
error.type = error_type; error.code = error_code;
error.details = error_details; error.metadata = metadata;
pending_call.is_error = true; pending_call.is_error = true;
pending_call.error = error; pending_call.error = error;
@@ -410,7 +396,8 @@ class Ajax {
// Network or server error - reject all pending calls // Network or server error - reject all pending calls
const error_message = Ajax._extract_error_message(xhr_error); const error_message = Ajax._extract_error_message(xhr_error);
const error = new Error(error_message); const error = new Error(error_message);
error.type = 'network_error'; error.code = Ajax.ERROR_NETWORK;
error.metadata = {};
for (const call_id in call_map) { for (const call_id in call_map) {
const pending_call = call_map[call_id]; const pending_call = call_map[call_id];

View File

@@ -30,8 +30,39 @@ class Debugger {
// Check if browser error logging is enabled // Check if browser error logging is enabled
if (window.rsxapp && window.rsxapp.log_browser_errors) { if (window.rsxapp && window.rsxapp.log_browser_errors) {
// Listen for unhandled exceptions from Rsx event system // Listen for unhandled exceptions from Rsx event system
Rsx.on('unhandled_exception', function (error_data) { Rsx.on('unhandled_exception', function (payload) {
Debugger._handle_browser_error(error_data); // 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 // Gets set to true to interupt startup sequence
static __stopped = false; static __stopped = false;
// Timestamp of last flash alert for unhandled exceptions (rate limiting)
static _last_exception_flash = 0;
// Initialize event handlers storage // Initialize event handlers storage
static _init_events() { static _init_events() {
if (typeof Rsx._event_handlers === 'undefined') { if (typeof Rsx._event_handlers === 'undefined') {
@@ -113,70 +110,101 @@ class Rsx {
/** /**
* Setup global unhandled exception handlers * Setup global unhandled exception handlers
* Must be called before framework initialization begins * 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() { static _setup_exception_handlers() {
// Handle uncaught JavaScript errors // Handle uncaught JavaScript errors
window.addEventListener('error', function (event) { window.addEventListener('error', function (event) {
Rsx._handle_unhandled_exception({ // Pass the Error object directly if available, otherwise create one
message: event.message, const exception = event.error || new Error(event.message);
filename: event.filename, // Attach additional metadata if not already present
lineno: event.lineno, if (!exception.filename) exception.filename = event.filename;
colno: event.colno, if (!exception.lineno) exception.lineno = event.lineno;
stack: event.error ? event.error.stack : null, if (!exception.colno) exception.colno = event.colno;
type: 'error',
error: event.error, Rsx._handle_unhandled_exception(exception, { source: 'window_error' });
});
}); });
// Handle unhandled promise rejections // Handle unhandled promise rejections
window.addEventListener('unhandledrejection', function (event) { window.addEventListener('unhandledrejection', function (event) {
Rsx._handle_unhandled_exception({ // event.reason can be Error, string, or any value
message: event.reason ? event.reason.message || String(event.reason) : 'Unhandled promise rejection', const exception = event.reason instanceof Error
stack: event.reason && event.reason.stack ? event.reason.stack : null, ? event.reason
type: 'unhandledrejection', : new Error(event.reason ? String(event.reason) : 'Unhandled promise rejection');
error: event.reason,
}); Rsx._handle_unhandled_exception(exception, { source: 'unhandled_rejection' });
}); });
} }
/** /**
* Internal handler for unhandled exceptions * 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) { static _handle_unhandled_exception(exception, meta = {}) {
// Always log to console // Trigger event for listeners:
console.error('[Rsx] Unhandled exception:', error_data); // - Debugger.js: Logs to server
// - Exception_Handler: Logs to console and displays error (layout or flash alert)
// Trigger event for listeners (e.g., Debugger for server logging) // Pass exception and metadata (source info for logging decisions)
Rsx.trigger('unhandled_exception', error_data); Rsx.trigger('unhandled_exception', { exception, meta });
// Disable SPA navigation if in SPA mode // Disable SPA navigation if in SPA mode
// This allows user to navigate away from broken page using normal browser navigation // This allows user to navigate away from broken page using normal browser navigation
if (typeof Spa !== 'undefined' && window.rsxapp && window.rsxapp.is_spa) { if (typeof Spa !== 'undefined' && window.rsxapp && window.rsxapp.is_spa) {
Spa.disable(); 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 // Log to server that an event happened
@@ -317,7 +345,19 @@ class Rsx {
// Check if route exists in PHP controller definitions // Check if route exists in PHP controller definitions
let pattern; let pattern;
if (Rsx._routes[class_name] && Rsx._routes[class_name][action_name]) { 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 { } else {
// Not found in PHP routes - check if it's a SPA action // Not found in PHP routes - check if it's a SPA action
pattern = Rsx._try_spa_action_route(class_name, params_obj); pattern = Rsx._try_spa_action_route(class_name, params_obj);

View File

@@ -2647,6 +2647,25 @@ class Manifest
$method_data['parameters'] = $parameters; $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; $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.' '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) // Flag to track if SPA is enabled (can be disabled on errors or dirty forms)
static _spa_enabled = true; 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 * Disable SPA navigation - all navigation becomes full page loads
* Call this when errors occur or forms are dirty * Call this when errors occur or forms are dirty
@@ -55,6 +58,52 @@ class Spa {
Spa._spa_enabled = true; 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 * Framework module initialization hook called during framework boot
* Only runs when window.rsxapp.is_spa === true * Only runs when window.rsxapp.is_spa === true
@@ -67,6 +116,9 @@ class Spa {
console_debug('Spa', 'Initializing Spa system'); console_debug('Spa', 'Initializing Spa system');
// Start 30-minute auto-disable timer
Spa._start_spa_timeout();
// Discover and register all action classes // Discover and register all action classes
Spa.discover_actions(); Spa.discover_actions();
@@ -324,13 +376,6 @@ class Spa {
// Get target URL (browser has already updated location) // Get target URL (browser has already updated location)
const url = window.location.pathname + window.location.search + window.location.hash; 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 // Retrieve scroll position from history state
const scroll = e.state?.scroll || null; const scroll = e.state?.scroll || null;
@@ -349,9 +394,11 @@ class Spa {
// const form_data = e.state?.form_data || {}; // const form_data = e.state?.form_data || {};
// Dispatch without modifying history (we're already at the target URL) // 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, { Spa.dispatch(url, {
history: 'none', history: 'none',
scroll: scroll scroll: scroll,
force: true
}); });
}); });
@@ -433,10 +480,12 @@ class Spa {
* - 'none': Don't modify history (used for back/forward) * - '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 {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.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 = {}) { static async dispatch(url, options = {}) {
// Check if SPA is disabled - do full page load // 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'); console.warn('[Spa.dispatch] SPA disabled, forcing full page load');
document.location.href = url; document.location.href = url;
return; return;
@@ -621,12 +670,18 @@ class Spa {
Spa.layout.stop(); Spa.layout.stop();
} }
// Clear body and create new layout // Clear spa-root and create new layout
$('body').empty(); // Note: We target #spa-root instead of body to preserve global UI containers
$('body').attr('class', ''); // (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 // 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 // Wait for layout to be ready
await Spa.layout.ready(); await Spa.layout.ready();

View File

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

View File

@@ -26,7 +26,53 @@ class Spa_Layout extends Component {
* @returns {jQuery} The content element * @returns {jQuery} The content element
*/ */
$content() { $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 // Clear content area
$content.empty(); $content.empty();
// Mark action as loading for exception display logic
this._action_is_loading = true;
try { try {
// Get the action class to check for @title decorator // Get the action class to check for @title decorator
const action_class = Manifest.get_class_by_name(action_name); 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 // Wait for action to be ready
await action.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) // Mark action as done loading
if (typeof Rsx !== 'undefined') { this._action_is_loading = false;
Rsx.trigger('unhandled_exception', { } catch (error) {
message: error.message, // Mark action as done loading (even though it failed)
stack: error.stack, this._action_is_loading = false;
type: 'action_lifecycle_error',
action_name: action_name, // Action lifecycle failed - log and trigger event
error: error, console.error('[Spa_Layout] Action lifecycle failed:', error);
});
}
// Disable SPA so forward navigation becomes full page loads // Disable SPA so forward navigation becomes full page loads
// (Back/forward still work as SPA to allow user to navigate away) // (Back/forward still work as SPA to allow user to navigate away)
@@ -131,16 +175,12 @@ class Spa_Layout extends Component {
Spa.disable(); Spa.disable();
} }
// Show error UI in content area so user can navigate away // Trigger global exception event
$content.html(` // Exception_Handler will decide how to display (layout or flash alert)
<div class="alert alert-danger m-4"> // Pass payload with exception (no meta.source = already logged above)
<h4>Page Failed to Load</h4> if (typeof Rsx !== 'undefined') {
<p>An error occurred while loading this page. You can navigate back or to another page.</p> Rsx.trigger('unhandled_exception', { exception: error, meta: {} });
<p class="mb-0"> }
<a href="javascript:history.back()" class="btn btn-secondary">Go Back</a>
</p>
</div>
`);
// Don't re-throw - allow navigation to continue working // 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 * Generate random first name
*/ */
#[Ajax_Endpoint] #[Ajax_Endpoint]
public static function first_name(Request $request, array $params = []): string public static function first_name(Request $request, array $params = [])
{ {
self::__check_production(); self::__check_production();
return self::__random_from_list('first_names'); return self::__random_from_list('first_names');
@@ -77,7 +77,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
* Generate random last name * Generate random last name
*/ */
#[Ajax_Endpoint] #[Ajax_Endpoint]
public static function last_name(Request $request, array $params = []): string public static function last_name(Request $request, array $params = [])
{ {
self::__check_production(); self::__check_production();
return self::__random_from_list('last_names'); return self::__random_from_list('last_names');
@@ -87,7 +87,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
* Generate random company name * Generate random company name
*/ */
#[Ajax_Endpoint] #[Ajax_Endpoint]
public static function company_name(Request $request, array $params = []): string public static function company_name(Request $request, array $params = [])
{ {
self::__check_production(); self::__check_production();
@@ -109,7 +109,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
* Generate random street address * Generate random street address
*/ */
#[Ajax_Endpoint] #[Ajax_Endpoint]
public static function address(Request $request, array $params = []): string public static function address(Request $request, array $params = [])
{ {
self::__check_production(); self::__check_production();
@@ -124,7 +124,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
* Generate random city name * Generate random city name
*/ */
#[Ajax_Endpoint] #[Ajax_Endpoint]
public static function city(Request $request, array $params = []): string public static function city(Request $request, array $params = [])
{ {
self::__check_production(); self::__check_production();
return self::__random_from_list('cities'); return self::__random_from_list('cities');
@@ -134,7 +134,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
* Generate random US state code * Generate random US state code
*/ */
#[Ajax_Endpoint] #[Ajax_Endpoint]
public static function state(Request $request, array $params = []): string public static function state(Request $request, array $params = [])
{ {
self::__check_production(); self::__check_production();
@@ -153,7 +153,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
* Generate random ZIP code * Generate random ZIP code
*/ */
#[Ajax_Endpoint] #[Ajax_Endpoint]
public static function zip(Request $request, array $params = []): string public static function zip(Request $request, array $params = [])
{ {
self::__check_production(); self::__check_production();
return str_pad((string)rand(10000, 99999), 5, '0', STR_PAD_LEFT); 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 * Avoids 555-000-0000 and any sequence containing 911
*/ */
#[Ajax_Endpoint] #[Ajax_Endpoint]
public static function phone(Request $request, array $params = []): string public static function phone(Request $request, array $params = [])
{ {
self::__check_production(); self::__check_production();
@@ -193,7 +193,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
* Format: wordwordnumbers+test@gmail.com * Format: wordwordnumbers+test@gmail.com
*/ */
#[Ajax_Endpoint] #[Ajax_Endpoint]
public static function email(Request $request, array $params = []): string public static function email(Request $request, array $params = [])
{ {
self::__check_production(); self::__check_production();
@@ -208,7 +208,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
* Generate random website URL * Generate random website URL
*/ */
#[Ajax_Endpoint] #[Ajax_Endpoint]
public static function website(Request $request, array $params = []): string public static function website(Request $request, array $params = [])
{ {
self::__check_production(); self::__check_production();
@@ -223,7 +223,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
* Generate random text/paragraph * Generate random text/paragraph
*/ */
#[Ajax_Endpoint] #[Ajax_Endpoint]
public static function text(Request $request, array $params = []): string public static function text(Request $request, array $params = [])
{ {
self::__check_production(); 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: * @param string $error_code One of Ajax::ERROR_* constants
* - HTTP requests: Sets flash alert and redirects to login * @param string|array|null $metadata Error message (string) or structured data (array)
* - AJAX requests: Returns JSON error with success:false * @return \App\RSpade\Core\Response\Error_Response
*
* @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
*/ */
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); return new \App\RSpade\Core\Response\Error_Response($error_code, $metadata);
}
/**
* 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);
} }
/** /**

View File

@@ -20,7 +20,7 @@ SYNOPSIS
on_ready() { on_ready() {
// All children ready, safe for DOM manipulation // 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> <Define:User_Card>
<div class="card"> <div class="card">
<img $id="avatar" src="<%= this.data.avatar %>" /> <img $sid="avatar" src="<%= this.data.avatar %>" />
<h3 $id="name"><%= this.data.name %></h3> <h3 $sid="name"><%= this.data.name %></h3>
<p $id="email"><%= this.data.email %></p> <p $sid="email"><%= this.data.email %></p>
<button $id="edit">Edit</button> <button $sid="edit">Edit</button>
</div> </div>
</Define:User_Card> </Define:User_Card>
@@ -94,9 +94,10 @@ TEMPLATE SYNTAX
<%= expression %> - Escaped HTML output (safe, default) <%= expression %> - Escaped HTML output (safe, default)
<%!= expression %> - Unescaped raw output (pre-sanitized content only) <%!= expression %> - Unescaped raw output (pre-sanitized content only)
<% statement; %> - JavaScript statements (loops, conditionals) <% statement; %> - JavaScript statements (loops, conditionals)
<%-- comment --%> - JQHTML comments (not HTML <!-- --> comments)
Attributes: 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) $attr=value - Component parameter (becomes this.args.attr)
Note: Also creates data-attr HTML attribute Note: Also creates data-attr HTML attribute
@event=this.method - Event binding (⚠️ verify functionality) @event=this.method - Event binding (⚠️ verify functionality)
@@ -255,7 +256,7 @@ THIS.ARGS VS THIS.DATA
Lifecycle Restrictions (ENFORCED): Lifecycle Restrictions (ENFORCED):
- on_create(): Can modify this.data (set defaults) - on_create(): Can modify this.data (set defaults)
- on_load(): Can ONLY access this.args and this.data - 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 Can modify this.data freely
- on_ready() / event handlers: Can modify this.args, read this.data - on_ready() / event handlers: Can modify this.args, read this.data
CANNOT modify this.data (frozen) CANNOT modify this.data (frozen)
@@ -286,7 +287,7 @@ THIS.ARGS VS THIS.DATA
on_ready() { on_ready() {
// Modify state, then reload // Modify state, then reload
this.$id('filter_btn').on('click', () => { this.$sid('filter_btn').on('click', () => {
this.args.filter = 'active'; // Change state this.args.filter = 'active'; // Change state
this.reload(); // Re-fetch with new state this.reload(); // Re-fetch with new state
}); });
@@ -375,6 +376,50 @@ CONTROL FLOW AND LOOPS
%> %>
<p>Total: $<%= total.toFixed(2) %></p> <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 COMPONENT LIFECYCLE
Five-stage deterministic lifecycle: Five-stage deterministic lifecycle:
@@ -403,7 +448,7 @@ COMPONENT LIFECYCLE
4. on_load() (bottom-up, siblings in parallel, CAN be async) 4. on_load() (bottom-up, siblings in parallel, CAN be async)
- Load async data based on this.args - Load async data based on this.args
- ONLY access this.args and this.data (RESTRICTED) - 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 - ONLY modify this.data - NEVER DOM
- NO child component access - NO child component access
- Siblings at same depth execute in parallel - Siblings at same depth execute in parallel
@@ -642,7 +687,7 @@ JAVASCRIPT COMPONENT CLASS
on_ready() { on_ready() {
// All children ready, safe for DOM // All children ready, safe for DOM
// Attach event handlers // 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); this.$.animate({opacity: 1}, 300);
} }
@@ -846,12 +891,12 @@ $REDRAWABLE ATTRIBUTE - LIGHTWEIGHT COMPONENTS
converts the element into a <Redrawable> component: converts the element into a <Redrawable> component:
<!-- Write this: --> <!-- Write this: -->
<div $redrawable $id="counter"> <div $redrawable $sid="counter">
Count: <%= this.data.count %> Count: <%= this.data.count %>
</div> </div>
<!-- Parser transforms to: --> <!-- Parser transforms to: -->
<Redrawable data-tag="div" $id="counter"> <Redrawable data-tag="div" $sid="counter">
Count: <%= this.data.count %> Count: <%= this.data.count %>
</Redrawable> </Redrawable>
@@ -867,12 +912,12 @@ $REDRAWABLE ATTRIBUTE - LIGHTWEIGHT COMPONENTS
async increment_counter() { async increment_counter() {
this.data.count++; this.data.count++;
// Re-render only the counter element, not entire dashboard // 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: 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 - Verifies element is a component (has $redrawable or is proper
component class) component class)
- Calls its render() method to update only that element - Calls its render() method to update only that element
@@ -881,7 +926,7 @@ $REDRAWABLE ATTRIBUTE - LIGHTWEIGHT COMPONENTS
- Parent component's DOM remains unchanged - Parent component's DOM remains unchanged
Error Handling: 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 - Clear error if element isn't configured as component
- Guides developers to correct usage patterns - Guides developers to correct usage patterns
@@ -922,7 +967,7 @@ LIFECYCLE MANIPULATION METHODS
} }
on_ready() { on_ready() {
this.$id('filter_btn').on('click', async () => { this.$sid('filter_btn').on('click', async () => {
this.args.filter = 'active'; // Update state this.args.filter = 'active'; // Update state
await this.reload(); // Re-fetch with new state await this.reload(); // Re-fetch with new state
}); });
@@ -1047,26 +1092,26 @@ DOM UTILITIES
jQuery wrapped component root element jQuery wrapped component root element
This is genuine jQuery - all methods work directly This is genuine jQuery - all methods work directly
this.$id(name) this.$sid(name)
Get scoped element as jQuery object 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 Returns jQuery object, NOT component instance
this.id(name) this.sid(name)
Get scoped child component instance directly 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 Returns component instance, NOT jQuery object
CRITICAL: this.$id() vs this.id() distinction CRITICAL: this.$sid() vs this.sid() distinction
- this.$id('foo') → jQuery object (for DOM manipulation) - this.$sid('foo') → jQuery object (for DOM manipulation)
- this.id('foo') → Component instance (for calling methods) - this.sid('foo') → Component instance (for calling methods)
Common mistake: Common mistake:
const comp = this.id('foo').component(); // ❌ WRONG const comp = this.sid('foo').component(); // ❌ WRONG
const comp = this.id('foo'); // ✅ CORRECT const comp = this.sid('foo'); // ✅ CORRECT
Getting component from jQuery: Getting component from jQuery:
const $elem = this.$id('foo'); const $elem = this.$sid('foo');
const comp = $elem.component(); // ✅ CORRECT (jQuery → component) const comp = $elem.component(); // ✅ CORRECT (jQuery → component)
this.data this.data
@@ -1105,13 +1150,13 @@ NESTING COMPONENTS
on_load, on_ready). on_load, on_ready).
SCOPED IDS SCOPED IDS
Use $id attribute for component-scoped element IDs: Use $sid attribute for component-scoped element IDs:
Template: Template:
<Define:User_Card> <Define:User_Card>
<h3 $id="title">Name</h3> <h3 $sid="title">Name</h3>
<p $id="email">Email</p> <p $sid="email">Email</p>
<button $id="edit_btn">Edit</button> <button $sid="edit_btn">Edit</button>
</Define:User_Card> </Define:User_Card>
Rendered HTML (automatic scoping): Rendered HTML (automatic scoping):
@@ -1121,13 +1166,13 @@ SCOPED IDS
<button id="edit_btn:c123">Edit</button> <button id="edit_btn:c123">Edit</button>
</div> </div>
Access with this.$id(): Access with this.$sid():
class User_Card extends Jqhtml_Component { class User_Card extends Jqhtml_Component {
on_ready() { on_ready() {
// Use logical name // Use logical name
this.$id('title').text('John Doe'); this.$sid('title').text('John Doe');
this.$id('email').text('john@example.com'); this.$sid('email').text('john@example.com');
this.$id('edit_btn').on('click', () => this.edit()); this.$sid('edit_btn').on('click', () => this.edit());
} }
} }
@@ -1148,12 +1193,12 @@ EXAMPLES
on_ready() { on_ready() {
// Attach event handlers after data loaded // Attach event handlers after data loaded
this.$id('buy').on('click', async () => { this.$sid('buy').on('click', async () => {
await Cart.add(this.data.id); 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'); this.$.toggleClass('favorited');
}); });
} }
@@ -1208,19 +1253,19 @@ EXAMPLES
} }
validate() { validate() {
const email = this.$id('email').val(); const email = this.$sid('email').val();
if (!email.includes('@')) { if (!email.includes('@')) {
this.$id('error').text('Invalid email'); this.$sid('error').text('Invalid email');
return false; return false;
} }
this.$id('error').text(''); this.$sid('error').text('');
return true; return true;
} }
async submit() { async submit() {
const data = { const data = {
email: this.$id('email').val(), email: this.$sid('email').val(),
message: this.$id('message').val(), message: this.$sid('message').val(),
}; };
await fetch('/contact', { await fetch('/contact', {
@@ -1391,6 +1436,70 @@ CONTENT AND SLOTS
- Form patterns with customizable field sets - Form patterns with customizable field sets
- Any component hierarchy with shared structure - 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 INTEGRATION WITH RSX
JQHTML automatically integrates with the RSX framework: JQHTML automatically integrates with the RSX framework:
- Templates discovered by manifest system during build - 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 ## 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. **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? ## What is RSpade?
**Visual Basic-like development for PHP/Laravel.** Think: VB6 apps → VB6 runtime → Windows = RSX apps → RSpade runtime → Laravel. **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 ### URL Generation & Navigation
@@ -363,6 +368,30 @@ class Contacts_View_Action extends Spa_Action {
Details: `php artisan rsx:man spa` 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 ## CONVERTING BLADE PAGES TO SPA ACTIONS
@@ -415,7 +444,7 @@ class Feature_Index_Action extends Spa_Action {
```php ```php
// Remove #[Route] method completely. Add Ajax endpoints: // Remove #[Route] method completely. Add Ajax endpoints:
#[Ajax_Endpoint] #[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()]; 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. directly in attribute context. Works with static values, interpolations, and multiple conditions per element.
Example: `<input <% if (this.args.required) { %>required="required"<% } %> />` 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) ### 🔴 State Management Rules (ENFORCED)
**this.args** - Component arguments (read-only in on_load(), modifiable elsewhere) **this.args** - Component arguments (read-only in on_load(), modifiable elsewhere)
@@ -702,21 +735,21 @@ async on_load() {
- **`$quoted="string"`** → String literal - **`$quoted="string"`** → String literal
- **`$unquoted=expression`** → JavaScript expression - **`$unquoted=expression`** → JavaScript expression
- **`$id="name"`** → Scoped element ID - **`$sid="name"`** → Scoped element ID
### Component Access ### Component Access
**this.$id(name)** → jQuery object (for DOM): **this.$sid(name)** → jQuery object (for DOM):
```javascript ```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 ```javascript
const comp = this.id('child'); // ✅ Returns component const comp = this.sid('child'); // ✅ Returns component
await comp.reload(); await comp.reload();
const comp = this.id('child').component(); // ❌ WRONG const comp = this.sid('child').component(); // ❌ WRONG
``` ```
### Incremental Scaffolding ### Incremental Scaffolding
@@ -741,7 +774,7 @@ const comp = this.id('child').component(); // ❌ WRONG
7. Use `Controller.method()` not `$.ajax()` 7. Use `Controller.method()` not `$.ajax()`
8. Blade components self-closing only 8. Blade components self-closing only
9. `on_create/render/stop` must be sync 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 ### Bundle Integration Required
@@ -786,11 +819,11 @@ class My_Form extends Component {
vals(values) { vals(values) {
if (values) { if (values) {
// Setter - populate form // Setter - populate form
this.$id('name').val(values.name || ''); this.$sid('name').val(values.name || '');
return null; return null;
} else { } else {
// Getter - extract values // 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): **Modal Classes** (for complex/reusable modals):
```javascript ```javascript
@@ -963,47 +996,49 @@ Details: `php artisan rsx:man file_upload`
## AJAX ENDPOINTS ## AJAX ENDPOINTS
**Return data directly - framework auto-wraps as `{success: true, data: ...}`**
```php ```php
#[Ajax_Endpoint] #[Ajax_Endpoint]
public static function fetch_client(Request $request, array $params = []) { public static function method(Request $request, array $params = []) {
$client = Client_Model::find($params['id']); return $data; // Success - framework wraps as {_success: true, _ajax_return_value: ...}
if (!$client) throw new \Exception('Not found');
return $client; // ✅ Return data directly
} }
``` ```
**❌ NEVER**: `return ['success' => true, 'data' => $client]` - framework adds this **Call:** `await Controller.method({param: value})`
**Special returns** (bypass auto-wrap): ### Error Responses
- `['errors' => [...]]` - Form validation errors
- `['redirect' => '/path']` - Client-side navigation Use `response_error(Ajax::ERROR_CODE, $metadata)`:
- `['error' => 'msg']` - Operation failure
```php ```php
// Form validation // Not found
if (empty($params['name'])) { return response_error(Ajax::ERROR_NOT_FOUND, 'Project not found');
return ['errors' => ['name' => 'Required']];
}
// Success with redirect // Validation
Flash_Alert::success('Saved'); return response_error(Ajax::ERROR_VALIDATION, [
return ['redirect' => Rsx::Route('View_Action', $id)]; 'email' => 'Invalid',
'name' => 'Required'
]);
// Permission error // Auto-message
if (!can_delete()) { return response_error(Ajax::ERROR_UNAUTHORIZED);
return ['error' => 'Permission denied']; ```
**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
}
} }
``` ```
--- Unhandled errors auto-show flash alert.
### 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.
--- ---

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

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

View File

@@ -27,7 +27,7 @@
- Access component methods via `this.methodName()` - Access component methods via `this.methodName()`
### Attribute System ### 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) - `$attr="value"``data-attr="value"` (data attributes)
- `@click="handler"` → jQuery event binding to component method - `@click="handler"` → jQuery event binding to component method
- `@submit.prevent="handler"` → Event with preventDefault - `@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) - `this._ready_state` - Lifecycle phase (0=created, 1=rendering, 2=creating, 3=loading, 4=ready)
### DOM Access Methods ### 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.$child('name')` - Get child component by name
- `this.$children()` - Get all direct child components - `this.$children()` - Get all direct child components
- `this.$parent()` - Get parent component - `this.$parent()` - Get parent component
@@ -260,11 +260,11 @@ class CustomInput extends Component {
val(value) { val(value) {
if (arguments.length === 0) { if (arguments.length === 0) {
// Getter - return processed value // Getter - return processed value
return this.parse_value(this.$id('input').val()); return this.parse_value(this.$sid('input').val());
} else { } else {
// Setter - validate and set // Setter - validate and set
if (this.validate(value)) { if (this.validate(value)) {
this.$id('input').val(this.format_value(value)); this.$sid('input').val(this.format_value(value));
this.data.value = value; this.data.value = value;
} }
return this.$; // Maintain jQuery chaining 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 { class DataComponent extends Component {
async on_create() { async on_create() {
// Set loading state in the DOM during create phase // 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() { async on_load() {
@@ -142,8 +142,8 @@ class DataComponent extends Component {
async on_ready() { async on_ready() {
// Update DOM after data is loaded // Update DOM after data is loaded
this.$id('status').removeClass('loading').text('Loaded'); this.$sid('status').removeClass('loading').text('Loaded');
this.$id('username').text(this.data.user.name); this.$sid('username').text(this.data.user.name);
} }
} }
``` ```
@@ -153,12 +153,12 @@ class DataComponent extends Component {
class BadComponent extends Component { class BadComponent extends Component {
async on_load() { async on_load() {
// ❌ WRONG - DOM modification in 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()); this.data.user = await fetch('/api/user').then(r => r.json());
// ❌ WRONG - More DOM modification // ❌ 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) { selectTab(tabId) {
// Use $id() for scoped selection // Use $id() for scoped selection
this.$id('tab1').removeClass('active'); this.$sid('tab1').removeClass('active');
this.$id('tab2').removeClass('active'); this.$sid('tab2').removeClass('active');
this.$id(tabId).addClass('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) - **@ Event attributes**: MUST be unquoted (pass function references)
Example: `@click=this.handleClick` Example: `@click=this.handleClick`
- **$ Data attributes**: Can be quoted OR unquoted (flexible) - **$ 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) - **Regular HTML attributes**: MUST be quoted (strings only)
Example: `class="container <%= this.args.theme %>"` Example: `class="container <%= this.args.theme %>"`
@@ -375,7 +375,7 @@ $('#user-profile').component().on('ready', (component) => {
console.log('User data:', component.data.user); console.log('User data:', component.data.user);
// Safe to access all DOM elements // 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_load(): Promise<void>;
on_ready(): Promise<void>; on_ready(): Promise<void>;
on_stop(): void | 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? * Should component re-render after load?
* By default, only re-renders if data has changed * 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" * Searches for elements with id="local_id:THIS_COMPONENT_CID"
* *
* Example: * Example:
* Template: <button $id="save_btn">Save</button> * Template: <button $sid="save_btn">Save</button>
* Rendered: <button id="save_btn:abc123" data-id="save_btn">Save</button> * Rendered: <button id="save_btn:abc123" data-sid="save_btn">Save</button>
* Access: this.$id('save_btn') // Returns jQuery element * Access: this.$sid('save_btn') // Returns jQuery element
* *
* Performance: Uses native document.getElementById() when component is in DOM, * Performance: Uses native document.getElementById() when component is in DOM,
* falls back to jQuery.find() for components not yet attached to 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) * @param local_id The local ID (without _cid suffix)
* @returns jQuery element with id="local_id:_cid", or empty jQuery object if not found * @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 * Get component instance by scoped ID
* *
* Convenience method that finds element by scoped ID and returns the component instance. * Convenience method that finds element by scoped ID and returns the component instance.
* *
* Example: * Example:
* Template: <User_Card $id="active_user" /> * Template: <User_Card $sid="active_user" />
* Access: const user = this.id('active_user'); // Returns User_Card instance * Access: const user = this.sid('active_user'); // Returns User_Card instance
* user.data.name // Access component's data * 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) * @param local_id The local ID (without _cid suffix)
* @returns Component instance or null if not found or not a component * @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) * Get the component that instantiated this component (rendered it in their template)
* Returns null if component was created programmatically via $().component() * 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":"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;IAiG7B;;;;;OAKG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAwR5B;;;;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;IAyK9B;;;;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;;;;;;;;;OASG;IACH,QAAQ,CAAC,IAAI,MAAM;IAEnB;;;;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,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,GAAG;IAgB3B;;;;;;;;;;;;;;;OAeG;IACH,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI;IAgB9C;;;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"}

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) { if (key === 'id' && tid) {
// Special handling for id attribute - scope to parent component's _cid // Special handling for id attribute - scope to parent component's _cid
// This is for regular id="foo" attributes that need scoping (rare case) // 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 :) // Don't double-scope if already scoped (contains :)
if (typeof value === 'string' && value.includes(':')) { if (typeof value === 'string' && value.includes(':')) {
html.push(` id="${value}"`); html.push(` id="${value}"`);
@@ -549,13 +549,13 @@ function process_component_to_html(instruction, html, components, context) {
// Create element with tracking ID // Create element with tracking ID
html.push(`<${tagName} data-cid="${cid}"`); html.push(`<${tagName} data-cid="${cid}"`);
// Handle id attributes for components // 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 // We just pass through what the compiler gave us - NEVER regenerate
if (props['data-id']) { if (props['data-sid']) {
const baseId = props['data-id']; const baseId = props['data-sid'];
// The compiled code ALWAYS sets props['id'] with the correct scoped value // 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 // 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 // Regular id passes through unchanged
else if (props['id']) { else if (props['id']) {
@@ -1229,16 +1229,16 @@ class Jqhtml_Component {
// If id provided, delegate to child component // If id provided, delegate to child component
if (id) { if (id) {
// First check if element with scoped ID exists // First check if element with scoped ID exists
const $element = this.$id(id); const $element = this.$sid(id);
if ($element.length === 0) { if ($element.length === 0) {
throw new Error(`[JQHTML] render("${id}") - no such id.\n` + 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 // Element exists, check if it's a component
const child = $element.data('_component'); const child = $element.data('_component');
if (!child) { if (!child) {
throw new Error(`[JQHTML] render("${id}") - element is not a component or does not have $redrawable attribute set.\n` + 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.`); `Add $redrawable attribute or make it a proper component.`);
} }
return child._render(); return child._render();
@@ -1459,15 +1459,15 @@ class Jqhtml_Component {
return; return;
// If id provided, delegate to child component // If id provided, delegate to child component
if (id) { if (id) {
const $element = this.$id(id); const $element = this.$sid(id);
if ($element.length === 0) { if ($element.length === 0) {
throw new Error(`[JQHTML] render("${id}") - no such id.\n` + 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'); const child = $element.data('_component');
if (!child) { if (!child) {
throw new Error(`[JQHTML] render("${id}") - element is not a component or does not have $redrawable attribute set.\n` + 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.`); `Add $redrawable attribute or make it a proper component.`);
} }
return child.render(); 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 // 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 { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; }); 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
// If cache_key is null, args are not serializable - skip caching 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) { if (cache_key === null) {
// Set data-nocache attribute for debugging (shows which property prevented caching) // Set data-nocache attribute for debugging (shows which property prevented caching)
if (uncacheable_property) { if (uncacheable_property) {
@@ -1574,8 +1592,25 @@ class Jqhtml_Component {
// Import coordinator and storage lazily to avoid circular dependency // Import coordinator and storage lazily to avoid circular dependency
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; }); const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; }); const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
// Generate cache key (same as deduplication key) // Check if component implements cache_id() for custom cache key
const { key: cache_key, uncacheable_property } = Load_Coordinator.generate_invocation_key(this.component_name(), this.args); 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 is null, args are not serializable - skip load deduplication and caching
if (cache_key === null) { if (cache_key === null) {
// Set data-nocache attribute for debugging (shows which property prevented caching) // Set data-nocache attribute for debugging (shows which property prevented caching)
@@ -1916,7 +1951,23 @@ class Jqhtml_Component {
if (args_changed) { if (args_changed) {
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; }); const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; }); 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 // Only use cache if args are serializable
if (cache_key !== null) { if (cache_key !== null) {
const cached_data = Jqhtml_Local_Storage.get(cache_key); const cached_data = Jqhtml_Local_Storage.get(cache_key);
@@ -1955,7 +2006,23 @@ class Jqhtml_Component {
if (data_changed && data_after_load !== '{}') { if (data_changed && data_after_load !== '{}') {
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; }); const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; }); 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 // Only update cache if args are serializable
if (cache_key !== null) { if (cache_key !== null) {
Jqhtml_Local_Storage.set(cache_key, this.data); 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" * Searches for elements with id="local_id:THIS_COMPONENT_CID"
* *
* Example: * Example:
* Template: <button $id="save_btn">Save</button> * Template: <button $sid="save_btn">Save</button>
* Rendered: <button id="save_btn:abc123" data-id="save_btn">Save</button> * Rendered: <button id="save_btn:abc123" data-sid="save_btn">Save</button>
* Access: this.$id('save_btn') // Returns jQuery element * Access: this.$sid('save_btn') // Returns jQuery element
* *
* Performance: Uses native document.getElementById() when component is in DOM, * Performance: Uses native document.getElementById() when component is in DOM,
* falls back to jQuery.find() for components not yet attached to 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) * @param local_id The local ID (without _cid suffix)
* @returns jQuery element with id="local_id:_cid", or empty jQuery object if not found * @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}`; const scopedId = `${local_id}:${this._cid}`;
// Try getElementById first (fast path - works when component is in DOM) // Try getElementById first (fast path - works when component is in DOM)
const el = document.getElementById(scopedId); const el = document.getElementById(scopedId);
@@ -2184,7 +2251,7 @@ class Jqhtml_Component {
return $(el); return $(el);
} }
// Fallback: component not in DOM yet, search within component subtree // 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 // Must escape the ID because it contains ':' which jQuery treats as a pseudo-selector
return this.$.find(`#${$.escapeSelector(scopedId)}`); 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. * Convenience method that finds element by scoped ID and returns the component instance.
* *
* Example: * Example:
* Template: <User_Card $id="active_user" /> * Template: <User_Card $sid="active_user" />
* Access: const user = this.id('active_user'); // Returns User_Card instance * Access: const user = this.sid('active_user'); // Returns User_Card instance
* user.data.name // Access component's data * 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) * @param local_id The local ID (without _cid suffix)
* @returns Component instance or null if not found or not a component * @returns Component instance or null if not found or not a component
*/ */
id(local_id) { sid(local_id) {
const element = this.$id(local_id); const element = this.$sid(local_id);
const component = element.data('_component'); const component = element.data('_component');
// If no component found but element exists, warn developer // If no component found but element exists, warn developer
if (!component && element.length > 0) { 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. ` + `${local_id} exists, however, it is not a component or $redrawable. ` +
`Did you forget to add $redrawable to the tag?`); `Did you forget to add $redrawable to the tag?`);
} }
@@ -2825,7 +2895,7 @@ function evaluate_expression(expression, component, locals = {}) {
args: component.args, args: component.args,
$: component.$, $: component.$,
// Component methods // Component methods
$id: component.$id.bind(component), $sid: component.$sid.bind(component),
// Locals (like $event) // Locals (like $event)
...locals ...locals
}; };
@@ -2853,7 +2923,7 @@ function evaluate_handler(expression, component) {
// Otherwise treat as inline code // Otherwise treat as inline code
try { try {
return new Function('$event', ` return new Function('$event', `
const { data, args, $, emit, $id } = this; const { data, args, $, emit, $sid } = this;
${expression} ${expression}
`).bind(component); `).bind(component);
} }
@@ -4151,7 +4221,7 @@ function init(jQuery) {
} }
} }
// Version - will be replaced during build with actual version from package.json // 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 // Default export with all functionality
const jqhtml = { const jqhtml = {
// Core // 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) { if (key === 'id' && tid) {
// Special handling for id attribute - scope to parent component's _cid // Special handling for id attribute - scope to parent component's _cid
// This is for regular id="foo" attributes that need scoping (rare case) // 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 :) // Don't double-scope if already scoped (contains :)
if (typeof value === 'string' && value.includes(':')) { if (typeof value === 'string' && value.includes(':')) {
html.push(` id="${value}"`); html.push(` id="${value}"`);
@@ -545,13 +545,13 @@ function process_component_to_html(instruction, html, components, context) {
// Create element with tracking ID // Create element with tracking ID
html.push(`<${tagName} data-cid="${cid}"`); html.push(`<${tagName} data-cid="${cid}"`);
// Handle id attributes for components // 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 // We just pass through what the compiler gave us - NEVER regenerate
if (props['data-id']) { if (props['data-sid']) {
const baseId = props['data-id']; const baseId = props['data-sid'];
// The compiled code ALWAYS sets props['id'] with the correct scoped value // 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 // 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 // Regular id passes through unchanged
else if (props['id']) { else if (props['id']) {
@@ -1225,16 +1225,16 @@ class Jqhtml_Component {
// If id provided, delegate to child component // If id provided, delegate to child component
if (id) { if (id) {
// First check if element with scoped ID exists // First check if element with scoped ID exists
const $element = this.$id(id); const $element = this.$sid(id);
if ($element.length === 0) { if ($element.length === 0) {
throw new Error(`[JQHTML] render("${id}") - no such id.\n` + 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 // Element exists, check if it's a component
const child = $element.data('_component'); const child = $element.data('_component');
if (!child) { if (!child) {
throw new Error(`[JQHTML] render("${id}") - element is not a component or does not have $redrawable attribute set.\n` + 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.`); `Add $redrawable attribute or make it a proper component.`);
} }
return child._render(); return child._render();
@@ -1455,15 +1455,15 @@ class Jqhtml_Component {
return; return;
// If id provided, delegate to child component // If id provided, delegate to child component
if (id) { if (id) {
const $element = this.$id(id); const $element = this.$sid(id);
if ($element.length === 0) { if ($element.length === 0) {
throw new Error(`[JQHTML] render("${id}") - no such id.\n` + 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'); const child = $element.data('_component');
if (!child) { if (!child) {
throw new Error(`[JQHTML] render("${id}") - element is not a component or does not have $redrawable attribute set.\n` + 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.`); `Add $redrawable attribute or make it a proper component.`);
} }
return child.render(); 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 // 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 { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; }); 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
// If cache_key is null, args are not serializable - skip caching 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) { if (cache_key === null) {
// Set data-nocache attribute for debugging (shows which property prevented caching) // Set data-nocache attribute for debugging (shows which property prevented caching)
if (uncacheable_property) { if (uncacheable_property) {
@@ -1570,8 +1588,25 @@ class Jqhtml_Component {
// Import coordinator and storage lazily to avoid circular dependency // Import coordinator and storage lazily to avoid circular dependency
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; }); const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; }); const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
// Generate cache key (same as deduplication key) // Check if component implements cache_id() for custom cache key
const { key: cache_key, uncacheable_property } = Load_Coordinator.generate_invocation_key(this.component_name(), this.args); 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 is null, args are not serializable - skip load deduplication and caching
if (cache_key === null) { if (cache_key === null) {
// Set data-nocache attribute for debugging (shows which property prevented caching) // Set data-nocache attribute for debugging (shows which property prevented caching)
@@ -1912,7 +1947,23 @@ class Jqhtml_Component {
if (args_changed) { if (args_changed) {
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; }); const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; }); 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 // Only use cache if args are serializable
if (cache_key !== null) { if (cache_key !== null) {
const cached_data = Jqhtml_Local_Storage.get(cache_key); const cached_data = Jqhtml_Local_Storage.get(cache_key);
@@ -1951,7 +2002,23 @@ class Jqhtml_Component {
if (data_changed && data_after_load !== '{}') { if (data_changed && data_after_load !== '{}') {
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; }); const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; }); 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 // Only update cache if args are serializable
if (cache_key !== null) { if (cache_key !== null) {
Jqhtml_Local_Storage.set(cache_key, this.data); 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" * Searches for elements with id="local_id:THIS_COMPONENT_CID"
* *
* Example: * Example:
* Template: <button $id="save_btn">Save</button> * Template: <button $sid="save_btn">Save</button>
* Rendered: <button id="save_btn:abc123" data-id="save_btn">Save</button> * Rendered: <button id="save_btn:abc123" data-sid="save_btn">Save</button>
* Access: this.$id('save_btn') // Returns jQuery element * Access: this.$sid('save_btn') // Returns jQuery element
* *
* Performance: Uses native document.getElementById() when component is in DOM, * Performance: Uses native document.getElementById() when component is in DOM,
* falls back to jQuery.find() for components not yet attached to 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) * @param local_id The local ID (without _cid suffix)
* @returns jQuery element with id="local_id:_cid", or empty jQuery object if not found * @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}`; const scopedId = `${local_id}:${this._cid}`;
// Try getElementById first (fast path - works when component is in DOM) // Try getElementById first (fast path - works when component is in DOM)
const el = document.getElementById(scopedId); const el = document.getElementById(scopedId);
@@ -2180,7 +2247,7 @@ class Jqhtml_Component {
return $(el); return $(el);
} }
// Fallback: component not in DOM yet, search within component subtree // 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 // Must escape the ID because it contains ':' which jQuery treats as a pseudo-selector
return this.$.find(`#${$.escapeSelector(scopedId)}`); 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. * Convenience method that finds element by scoped ID and returns the component instance.
* *
* Example: * Example:
* Template: <User_Card $id="active_user" /> * Template: <User_Card $sid="active_user" />
* Access: const user = this.id('active_user'); // Returns User_Card instance * Access: const user = this.sid('active_user'); // Returns User_Card instance
* user.data.name // Access component's data * 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) * @param local_id The local ID (without _cid suffix)
* @returns Component instance or null if not found or not a component * @returns Component instance or null if not found or not a component
*/ */
id(local_id) { sid(local_id) {
const element = this.$id(local_id); const element = this.$sid(local_id);
const component = element.data('_component'); const component = element.data('_component');
// If no component found but element exists, warn developer // If no component found but element exists, warn developer
if (!component && element.length > 0) { 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. ` + `${local_id} exists, however, it is not a component or $redrawable. ` +
`Did you forget to add $redrawable to the tag?`); `Did you forget to add $redrawable to the tag?`);
} }
@@ -2821,7 +2891,7 @@ function evaluate_expression(expression, component, locals = {}) {
args: component.args, args: component.args,
$: component.$, $: component.$,
// Component methods // Component methods
$id: component.$id.bind(component), $sid: component.$sid.bind(component),
// Locals (like $event) // Locals (like $event)
...locals ...locals
}; };
@@ -2849,7 +2919,7 @@ function evaluate_handler(expression, component) {
// Otherwise treat as inline code // Otherwise treat as inline code
try { try {
return new Function('$event', ` return new Function('$event', `
const { data, args, $, emit, $id } = this; const { data, args, $, emit, $sid } = this;
${expression} ${expression}
`).bind(component); `).bind(component);
} }
@@ -4147,7 +4217,7 @@ function init(jQuery) {
} }
} }
// Version - will be replaced during build with actual version from package.json // 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 // Default export with all functionality
const jqhtml = { const jqhtml = {
// Core // 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 * (c) 2025 JQHTML Team
* Released under the MIT License * Released under the MIT License
*/ */
@@ -500,7 +500,7 @@ function process_tag_to_html(instruction, html, tagElements, components, context
if (key === 'id' && tid) { if (key === 'id' && tid) {
// Special handling for id attribute - scope to parent component's _cid // Special handling for id attribute - scope to parent component's _cid
// This is for regular id="foo" attributes that need scoping (rare case) // 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 :) // Don't double-scope if already scoped (contains :)
if (typeof value === 'string' && value.includes(':')) { if (typeof value === 'string' && value.includes(':')) {
html.push(` id="${value}"`); html.push(` id="${value}"`);
@@ -550,13 +550,13 @@ function process_component_to_html(instruction, html, components, context) {
// Create element with tracking ID // Create element with tracking ID
html.push(`<${tagName} data-cid="${cid}"`); html.push(`<${tagName} data-cid="${cid}"`);
// Handle id attributes for components // 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 // We just pass through what the compiler gave us - NEVER regenerate
if (props['data-id']) { if (props['data-sid']) {
const baseId = props['data-id']; const baseId = props['data-sid'];
// The compiled code ALWAYS sets props['id'] with the correct scoped value // 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 // 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 // Regular id passes through unchanged
else if (props['id']) { else if (props['id']) {
@@ -1230,16 +1230,16 @@ class Jqhtml_Component {
// If id provided, delegate to child component // If id provided, delegate to child component
if (id) { if (id) {
// First check if element with scoped ID exists // First check if element with scoped ID exists
const $element = this.$id(id); const $element = this.$sid(id);
if ($element.length === 0) { if ($element.length === 0) {
throw new Error(`[JQHTML] render("${id}") - no such id.\n` + 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 // Element exists, check if it's a component
const child = $element.data('_component'); const child = $element.data('_component');
if (!child) { if (!child) {
throw new Error(`[JQHTML] render("${id}") - element is not a component or does not have $redrawable attribute set.\n` + 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.`); `Add $redrawable attribute or make it a proper component.`);
} }
return child._render(); return child._render();
@@ -1460,15 +1460,15 @@ class Jqhtml_Component {
return; return;
// If id provided, delegate to child component // If id provided, delegate to child component
if (id) { if (id) {
const $element = this.$id(id); const $element = this.$sid(id);
if ($element.length === 0) { if ($element.length === 0) {
throw new Error(`[JQHTML] render("${id}") - no such id.\n` + 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'); const child = $element.data('_component');
if (!child) { if (!child) {
throw new Error(`[JQHTML] render("${id}") - element is not a component or does not have $redrawable attribute set.\n` + 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.`); `Add $redrawable attribute or make it a proper component.`);
} }
return child.render(); 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 // 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 { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; }); 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
// If cache_key is null, args are not serializable - skip caching 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) { if (cache_key === null) {
// Set data-nocache attribute for debugging (shows which property prevented caching) // Set data-nocache attribute for debugging (shows which property prevented caching)
if (uncacheable_property) { if (uncacheable_property) {
@@ -1575,8 +1593,25 @@ class Jqhtml_Component {
// Import coordinator and storage lazily to avoid circular dependency // Import coordinator and storage lazily to avoid circular dependency
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; }); const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; }); const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
// Generate cache key (same as deduplication key) // Check if component implements cache_id() for custom cache key
const { key: cache_key, uncacheable_property } = Load_Coordinator.generate_invocation_key(this.component_name(), this.args); 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 is null, args are not serializable - skip load deduplication and caching
if (cache_key === null) { if (cache_key === null) {
// Set data-nocache attribute for debugging (shows which property prevented caching) // Set data-nocache attribute for debugging (shows which property prevented caching)
@@ -1917,7 +1952,23 @@ class Jqhtml_Component {
if (args_changed) { if (args_changed) {
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; }); const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; }); 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 // Only use cache if args are serializable
if (cache_key !== null) { if (cache_key !== null) {
const cached_data = Jqhtml_Local_Storage.get(cache_key); const cached_data = Jqhtml_Local_Storage.get(cache_key);
@@ -1956,7 +2007,23 @@ class Jqhtml_Component {
if (data_changed && data_after_load !== '{}') { if (data_changed && data_after_load !== '{}') {
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; }); const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; }); 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 // Only update cache if args are serializable
if (cache_key !== null) { if (cache_key !== null) {
Jqhtml_Local_Storage.set(cache_key, this.data); 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" * Searches for elements with id="local_id:THIS_COMPONENT_CID"
* *
* Example: * Example:
* Template: <button $id="save_btn">Save</button> * Template: <button $sid="save_btn">Save</button>
* Rendered: <button id="save_btn:abc123" data-id="save_btn">Save</button> * Rendered: <button id="save_btn:abc123" data-sid="save_btn">Save</button>
* Access: this.$id('save_btn') // Returns jQuery element * Access: this.$sid('save_btn') // Returns jQuery element
* *
* Performance: Uses native document.getElementById() when component is in DOM, * Performance: Uses native document.getElementById() when component is in DOM,
* falls back to jQuery.find() for components not yet attached to 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) * @param local_id The local ID (without _cid suffix)
* @returns jQuery element with id="local_id:_cid", or empty jQuery object if not found * @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}`; const scopedId = `${local_id}:${this._cid}`;
// Try getElementById first (fast path - works when component is in DOM) // Try getElementById first (fast path - works when component is in DOM)
const el = document.getElementById(scopedId); const el = document.getElementById(scopedId);
@@ -2185,7 +2252,7 @@ class Jqhtml_Component {
return $(el); return $(el);
} }
// Fallback: component not in DOM yet, search within component subtree // 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 // Must escape the ID because it contains ':' which jQuery treats as a pseudo-selector
return this.$.find(`#${$.escapeSelector(scopedId)}`); 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. * Convenience method that finds element by scoped ID and returns the component instance.
* *
* Example: * Example:
* Template: <User_Card $id="active_user" /> * Template: <User_Card $sid="active_user" />
* Access: const user = this.id('active_user'); // Returns User_Card instance * Access: const user = this.sid('active_user'); // Returns User_Card instance
* user.data.name // Access component's data * 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) * @param local_id The local ID (without _cid suffix)
* @returns Component instance or null if not found or not a component * @returns Component instance or null if not found or not a component
*/ */
id(local_id) { sid(local_id) {
const element = this.$id(local_id); const element = this.$sid(local_id);
const component = element.data('_component'); const component = element.data('_component');
// If no component found but element exists, warn developer // If no component found but element exists, warn developer
if (!component && element.length > 0) { 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. ` + `${local_id} exists, however, it is not a component or $redrawable. ` +
`Did you forget to add $redrawable to the tag?`); `Did you forget to add $redrawable to the tag?`);
} }
@@ -2826,7 +2896,7 @@ function evaluate_expression(expression, component, locals = {}) {
args: component.args, args: component.args,
$: component.$, $: component.$,
// Component methods // Component methods
$id: component.$id.bind(component), $sid: component.$sid.bind(component),
// Locals (like $event) // Locals (like $event)
...locals ...locals
}; };
@@ -2854,7 +2924,7 @@ function evaluate_handler(expression, component) {
// Otherwise treat as inline code // Otherwise treat as inline code
try { try {
return new Function('$event', ` return new Function('$event', `
const { data, args, $, emit, $id } = this; const { data, args, $, emit, $sid } = this;
${expression} ${expression}
`).bind(component); `).bind(component);
} }
@@ -4152,7 +4222,7 @@ function init(jQuery) {
} }
} }
// Version - will be replaced during build with actual version from package.json // 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 // Default export with all functionality
const jqhtml = { const jqhtml = {
// Core // 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", "name": "@jqhtml/core",
"version": "2.2.217", "version": "2.2.218",
"description": "Core runtime library for JQHTML", "description": "Core runtime library for JQHTML",
"type": "module", "type": "module",
"main": "./dist/index.js", "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 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 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 ```jqhtml
<Define:UserCard> <Define:UserCard>
<div $id="container"> <div $sid="container">
<input $id="username" type="text" /> <input $sid="username" type="text" />
<button $id="submit">Submit</button> <button $sid="submit">Submit</button>
</div> </div>
</Define:UserCard> </Define:UserCard>
``` ```
@@ -207,8 +207,8 @@ function render(_cid) {
class UserCard extends Component { class UserCard extends Component {
init() { init() {
// Find scoped elements // Find scoped elements
const $username = this.$id('username'); // Returns $('#username:123') const $username = this.$sid('username'); // Returns $('#username:123')
const $submit = this.$id('submit'); // Returns $('#submit:123') const $submit = this.$sid('submit'); // Returns $('#submit:123')
$submit.on('click', () => { $submit.on('click', () => {
const value = $username.val(); const value = $username.val();
@@ -224,15 +224,15 @@ Component IDs flow through lexical scope naturally:
```jqhtml ```jqhtml
<Define:ParentComponent> <Define:ParentComponent>
<div $id="parent-element"> <!-- Gets ParentComponent's _cid --> <div $sid="parent-element"> <!-- Gets ParentComponent's _cid -->
<ChildComponent> <ChildComponent>
<div $id="slot-element" /> <!-- Also gets ParentComponent's _cid --> <div $sid="slot-element" /> <!-- Also gets ParentComponent's _cid -->
</ChildComponent> </ChildComponent>
</div> </div>
</Define:ParentComponent> </Define:ParentComponent>
<Define:ChildComponent> <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 --> <%= content() %> <!-- Slot content preserves parent's _cid -->
</div> </div>
</Define:ChildComponent> </Define:ChildComponent>
@@ -374,9 +374,9 @@ The `@` prefix binds event handlers:
<!-- Wrapper that adds behavior to innerHTML --> <!-- Wrapper that adds behavior to innerHTML -->
<Define:Collapsible> <Define:Collapsible>
<div class="collapsible" $id="wrapper"> <div class="collapsible" $sid="wrapper">
<button $onclick="toggle">Toggle</button> <button $onclick="toggle">Toggle</button>
<div class="content" $id="content"> <div class="content" $sid="content">
<%= content() %> <!-- Wrapped innerHTML --> <%= content() %> <!-- Wrapped innerHTML -->
</div> </div>
</div> </div>
@@ -481,7 +481,7 @@ Templates compile to three instruction types:
{tag: ["div", {"class": "user-card"}, false]} {tag: ["div", {"class": "user-card"}, false]}
{tag: ["img", {"src": "/avatar.jpg", "alt": "User"}, true]} {tag: ["img", {"src": "/avatar.jpg", "alt": "User"}, true]}
{tag: ["button", {"onclick": this.handleClick}, false]} {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 ### 2. Component Instruction

View File

@@ -390,7 +390,7 @@ These patterns inform v2 implementation decisions, particularly around maintaini
- **Expressions**: `<%= expression %>` for output - **Expressions**: `<%= expression %>` for output
- **Bindings**: `:text`, `:value`, `:class`, `:style` for data binding - **Bindings**: `:text`, `:value`, `:class`, `:style` for data binding
- **Events**: `@click`, `@change`, etc. for event handlers - **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 ### 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]) => { const entries = Object.entries(attrs).flatMap(([key, value]) => {
// Convert 'tag' to '_tag' for component invocations // Convert 'tag' to '_tag' for component invocations
const attrKey = key === 'tag' ? '_tag' : key; const attrKey = key === 'tag' ? '_tag' : key;
// Special handling for data-id attribute (from $id) - create scoped id // Special handling for data-sid attribute (from $sid) - create scoped id
// NOTE: Parser converts $id="foo" → data-id="foo" so we can distinguish from regular id // NOTE: Parser converts $sid="foo" → data-sid="foo" so we can distinguish from regular id
// This generates: id="foo:PARENT_CID" data-id="foo" // This generates: id="foo:PARENT_CID" data-sid="foo"
// The :PARENT_CID scoping happens at runtime in instruction-processor.ts // The :PARENT_CID scoping happens at runtime in instruction-processor.ts
if (key === 'data-id') { if (key === 'data-sid') {
const id_entries = []; const id_entries = [];
if (value && typeof value === 'object' && value.interpolated) { 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) => { const parts = value.parts.map((part) => {
if (part.type === 'text') { if (part.type === 'text') {
return this.escape_string(part.value); return this.escape_string(part.value);
@@ -1060,19 +1060,19 @@ export class CodeGenerator {
}); });
const base_id = parts.join(' + '); const base_id = parts.join(' + ');
id_entries.push(`"id": ${base_id} + ":" + this._cid`); 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) { 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); const base_id = this.escape_string(value.value);
id_entries.push(`"id": ${base_id} + ":" + this._cid`); id_entries.push(`"id": ${base_id} + ":" + this._cid`);
id_entries.push(`"data-id": ${base_id}`); id_entries.push(`"data-sid": ${base_id}`);
} }
else { 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)); const base_id = this.escape_string(String(value));
id_entries.push(`"id": ${base_id} + ":" + this._cid`); 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; return id_entries;
} }
@@ -1348,7 +1348,7 @@ export class CodeGenerator {
for (const [name, component] of this.components) { for (const [name, component] of this.components) {
code += `// Component: ${name}\n`; code += `// Component: ${name}\n`;
code += `jqhtml_components.set('${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 += ` name: '${name}',\n`;
code += ` tag: '${component.tagName}',\n`; code += ` tag: '${component.tagName}',\n`;
code += ` defaultAttributes: ${this.serializeAttributeObject(component.defaultAttributes)},\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()) { while (!this.check(TokenType.GT) && !this.is_at_end()) {
const attr_name = this.consume(TokenType.ATTR_NAME, 'Expected attribute name'); const attr_name = this.consume(TokenType.ATTR_NAME, 'Expected attribute name');
// Validate that $id is not used in Define tags // Validate that $sid is not used in Define tags
if (attr_name.value === '$id') { if (attr_name.value === '$sid') {
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); 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 ='); this.consume(TokenType.EQUALS, 'Expected =');
const attr_value = this.parse_attribute_value(); const attr_value = this.parse_attribute_value();
@@ -535,10 +535,10 @@ export class Parser {
} }
// Handle special attribute prefixes // Handle special attribute prefixes
if (name.startsWith('$')) { 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) // All other $ attributes stay as-is (handled by instruction-processor.ts)
if (name === '$id') { if (name === '$sid') {
name = 'data-id'; name = 'data-sid';
} }
// Keep $ prefix for other attributes - they get stored via .data() at runtime // Keep $ prefix for other attributes - they get stored via .data() at runtime
// Keep the value object intact to preserve quoted/unquoted distinction // 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' + ' Or set attributes in on_ready() using jQuery:\n' +
' ✅ <tag $id="my_element">\n' + ' ✅ <tag $id="my_element">\n' +
' on_ready() {\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; 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