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:
@@ -180,11 +180,11 @@ class Component_Create_Command extends Command
|
||||
$display_name = str_replace('_Component', '', $class_name);
|
||||
|
||||
return <<<JQHTML
|
||||
<!--
|
||||
<%--
|
||||
{$class_name}
|
||||
|
||||
\$button_text="Click Me" - Text shown on the button
|
||||
-->
|
||||
--%>
|
||||
<Define:{$class_name} style="border: 1px solid black;">
|
||||
<h4>{$display_name}</h4>
|
||||
<div \$id="hello_world" style="font-weight: bold; display: none;">
|
||||
|
||||
@@ -29,11 +29,42 @@ use App\RSpade\Core\Response\Rsx_Response_Abstract;
|
||||
*/
|
||||
class Ajax
|
||||
{
|
||||
// Error code constants
|
||||
const ERROR_VALIDATION = 'validation';
|
||||
const ERROR_NOT_FOUND = 'not_found';
|
||||
const ERROR_UNAUTHORIZED = 'unauthorized';
|
||||
const ERROR_AUTH_REQUIRED = 'auth_required';
|
||||
const ERROR_FATAL = 'fatal';
|
||||
const ERROR_GENERIC = 'generic';
|
||||
const ERROR_SERVER = 'server_error'; // Client-generated (HTTP 500)
|
||||
const ERROR_NETWORK = 'network_error'; // Client-generated (connection failed)
|
||||
|
||||
/**
|
||||
* Flag to indicate AJAX response mode for error handlers
|
||||
*/
|
||||
protected static bool $ajax_response_mode = false;
|
||||
|
||||
/**
|
||||
* Get default message for error code
|
||||
*
|
||||
* @param string $error_code One of the ERROR_* constants
|
||||
* @return string Default user-friendly message
|
||||
*/
|
||||
public static function get_default_message(string $error_code): string
|
||||
{
|
||||
return match ($error_code) {
|
||||
self::ERROR_VALIDATION => 'Please correct the errors below',
|
||||
self::ERROR_NOT_FOUND => 'The requested record was not found',
|
||||
self::ERROR_UNAUTHORIZED => 'You do not have permission to perform this action',
|
||||
self::ERROR_AUTH_REQUIRED => 'Please log in to continue',
|
||||
self::ERROR_FATAL => 'A fatal error has occurred',
|
||||
self::ERROR_SERVER => 'A server error occurred. Please try again.',
|
||||
self::ERROR_NETWORK => 'Could not connect to server. Please check your connection.',
|
||||
self::ERROR_GENERIC => 'An error has occurred',
|
||||
default => 'An error has occurred',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Call an internal API method directly from PHP code
|
||||
*
|
||||
@@ -157,19 +188,26 @@ class Ajax
|
||||
$details = $response->get_details();
|
||||
|
||||
switch ($type) {
|
||||
case 'response_auth_required':
|
||||
case self::ERROR_AUTH_REQUIRED:
|
||||
throw new AjaxAuthRequiredException($reason);
|
||||
case 'response_unauthorized':
|
||||
|
||||
case self::ERROR_UNAUTHORIZED:
|
||||
throw new AjaxUnauthorizedException($reason);
|
||||
case 'response_form_error':
|
||||
|
||||
case self::ERROR_VALIDATION:
|
||||
case self::ERROR_NOT_FOUND:
|
||||
throw new AjaxFormErrorException($reason, $details);
|
||||
case 'fatal':
|
||||
|
||||
case self::ERROR_FATAL:
|
||||
$message = $reason;
|
||||
if (!empty($details)) {
|
||||
$message .= ' - ' . json_encode($details);
|
||||
}
|
||||
|
||||
throw new AjaxFatalErrorException($message);
|
||||
|
||||
case self::ERROR_GENERIC:
|
||||
throw new Exception($reason);
|
||||
|
||||
default:
|
||||
throw new Exception("Unknown RSX response type: {$type}");
|
||||
}
|
||||
@@ -324,18 +362,14 @@ class Ajax
|
||||
throw new Exception($message);
|
||||
}
|
||||
|
||||
// Build error response based on type
|
||||
// Build error response
|
||||
$json_response = [
|
||||
'_success' => false,
|
||||
'error_type' => $type,
|
||||
'error_code' => $response->get_error_code(),
|
||||
'reason' => $response->get_reason(),
|
||||
'metadata' => $response->get_metadata(),
|
||||
];
|
||||
|
||||
// Add details for form errors
|
||||
if ($type === 'response_form_error') {
|
||||
$json_response['details'] = $response->get_details();
|
||||
}
|
||||
|
||||
// Add console debug messages if any
|
||||
$console_messages = \App\RSpade\Core\Debug\Debugger::_get_console_messages();
|
||||
if (!empty($console_messages)) {
|
||||
@@ -395,7 +429,7 @@ class Ajax
|
||||
if ((array_key_exists('_success', $response) && is_bool($response['_success'])) ||
|
||||
(array_key_exists('success', $response) && is_bool($response['success']))) {
|
||||
$wrong_way = "return ['_success' => false, 'message' => 'Error'];";
|
||||
$right_way_validation = "return response_form_error('Validation failed', ['email' => 'Invalid']);";
|
||||
$right_way_validation = "return response_error(Ajax::ERROR_VALIDATION, ['email' => 'Invalid email']);";
|
||||
$right_way_success = "return ['user_id' => 123, 'data' => [...]];\n// Framework wraps: {_success: true, _ajax_return_value: {...}}";
|
||||
$right_way_exception = "// Let exceptions bubble - framework handles them\n\$user->save(); // Don't wrap in try/catch";
|
||||
|
||||
|
||||
@@ -101,30 +101,33 @@ class Ajax_Batch_Controller extends Rsx_Controller_Abstract
|
||||
} catch (Exceptions\AjaxAuthRequiredException $e) {
|
||||
$responses["C_{$call_id}"] = [
|
||||
'success' => false,
|
||||
'error_type' => 'response_auth_required',
|
||||
'error_code' => Ajax::ERROR_AUTH_REQUIRED,
|
||||
'reason' => $e->getMessage(),
|
||||
'metadata' => [],
|
||||
];
|
||||
|
||||
} catch (Exceptions\AjaxUnauthorizedException $e) {
|
||||
$responses["C_{$call_id}"] = [
|
||||
'success' => false,
|
||||
'error_type' => 'response_unauthorized',
|
||||
'error_code' => Ajax::ERROR_UNAUTHORIZED,
|
||||
'reason' => $e->getMessage(),
|
||||
'metadata' => [],
|
||||
];
|
||||
|
||||
} catch (Exceptions\AjaxFormErrorException $e) {
|
||||
$responses["C_{$call_id}"] = [
|
||||
'success' => false,
|
||||
'error_type' => 'response_form_error',
|
||||
'error_code' => Ajax::ERROR_VALIDATION,
|
||||
'reason' => $e->getMessage(),
|
||||
'details' => $e->get_details(),
|
||||
'metadata' => $e->get_details(),
|
||||
];
|
||||
|
||||
} catch (Exceptions\AjaxFatalErrorException $e) {
|
||||
$responses["C_{$call_id}"] = [
|
||||
'success' => false,
|
||||
'error_type' => 'fatal',
|
||||
'error_code' => Ajax::ERROR_FATAL,
|
||||
'reason' => $e->getMessage(),
|
||||
'metadata' => [],
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
|
||||
@@ -2176,16 +2176,20 @@ JS;
|
||||
foreach ($file_info['public_static_methods'] ?? [] as $method_name => $method_data) {
|
||||
foreach ($method_data['attributes'] ?? [] as $attr_name => $attr_instances) {
|
||||
if ($attr_name === 'Route') {
|
||||
// Get the first route pattern (index 0 is the first argument)
|
||||
// Collect all route patterns for this method (supports multiple #[Route] attributes)
|
||||
foreach ($attr_instances as $instance) {
|
||||
$route_pattern = $instance[0] ?? null;
|
||||
if ($route_pattern) {
|
||||
console_debug('BUNDLE', " Found route: {$class_name}::{$method_name} => {$route_pattern}");
|
||||
// Store route info
|
||||
// Initialize arrays if needed
|
||||
if (!isset($routes[$class_name])) {
|
||||
$routes[$class_name] = [];
|
||||
}
|
||||
$routes[$class_name][$method_name] = $route_pattern;
|
||||
if (!isset($routes[$class_name][$method_name])) {
|
||||
$routes[$class_name][$method_name] = [];
|
||||
}
|
||||
// Append route pattern to array
|
||||
$routes[$class_name][$method_name][] = $route_pattern;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2231,15 +2235,19 @@ JS;
|
||||
foreach ($file_info['public_static_methods'] ?? [] as $method_name => $method_data) {
|
||||
foreach ($method_data['attributes'] ?? [] as $attr_name => $attr_instances) {
|
||||
if ($attr_name === 'Route') {
|
||||
// Get the first route pattern (index 0 is the first argument)
|
||||
// Collect all route patterns for this method (supports multiple #[Route] attributes)
|
||||
foreach ($attr_instances as $instance) {
|
||||
$route_pattern = $instance[0] ?? null;
|
||||
if ($route_pattern) {
|
||||
// Store route info
|
||||
// Initialize arrays if needed
|
||||
if (!isset($routes[$class_name])) {
|
||||
$routes[$class_name] = [];
|
||||
}
|
||||
$routes[$class_name][$method_name] = $route_pattern;
|
||||
if (!isset($routes[$class_name][$method_name])) {
|
||||
$routes[$class_name][$method_name] = [];
|
||||
}
|
||||
// Append route pattern to array
|
||||
$routes[$class_name][$method_name][] = $route_pattern;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ class Rsx_Reference_Data_Controller extends Rsx_Controller_Abstract
|
||||
* Returns array of {value: country_code, label: country_name} sorted alphabetically
|
||||
*/
|
||||
#[Ajax_Endpoint]
|
||||
public static function countries(Request $request, array $params = []): array
|
||||
public static function countries(Request $request, array $params = [])
|
||||
{
|
||||
return Country_Model::enabled()
|
||||
->orderBy('name')
|
||||
@@ -41,10 +41,9 @@ class Rsx_Reference_Data_Controller extends Rsx_Controller_Abstract
|
||||
*
|
||||
* @param Request $request
|
||||
* @param array $params - Expected: ['country' => 'US']
|
||||
* @return array
|
||||
*/
|
||||
#[Ajax_Endpoint]
|
||||
public static function states(Request $request, array $params = []): array
|
||||
public static function states(Request $request, array $params = [])
|
||||
{
|
||||
$country = $params['country'] ?? 'US';
|
||||
|
||||
|
||||
@@ -19,10 +19,9 @@ class Debugger_Controller extends Rsx_Controller_Abstract
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param array $params
|
||||
* @return array
|
||||
*/
|
||||
#[Ajax_Endpoint]
|
||||
public static function log_console_messages(Request $request, array $params = []): array
|
||||
public static function log_console_messages(Request $request, array $params = [])
|
||||
{
|
||||
return Debugger::log_console_messages($request, $params);
|
||||
}
|
||||
@@ -32,10 +31,9 @@ class Debugger_Controller extends Rsx_Controller_Abstract
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param array $params
|
||||
* @return array
|
||||
*/
|
||||
#[Ajax_Endpoint]
|
||||
public static function log_browser_errors(Request $request, array $params = []): array
|
||||
public static function log_browser_errors(Request $request, array $params = [])
|
||||
{
|
||||
return Debugger::log_browser_errors($request, $params);
|
||||
}
|
||||
|
||||
@@ -782,7 +782,7 @@ class Dispatcher
|
||||
}
|
||||
|
||||
// Handle authentication required
|
||||
if ($type === 'response_auth_required') {
|
||||
if ($type === \App\RSpade\Core\Ajax\Ajax::ERROR_AUTH_REQUIRED) {
|
||||
if ($redirect_url) {
|
||||
Rsx::flash_error($reason);
|
||||
|
||||
@@ -793,7 +793,7 @@ class Dispatcher
|
||||
}
|
||||
|
||||
// Handle unauthorized
|
||||
if ($type === 'response_unauthorized') {
|
||||
if ($type === \App\RSpade\Core\Ajax\Ajax::ERROR_UNAUTHORIZED) {
|
||||
if ($redirect_url) {
|
||||
Rsx::flash_error($reason);
|
||||
|
||||
@@ -803,8 +803,8 @@ class Dispatcher
|
||||
throw new Exception($reason);
|
||||
}
|
||||
|
||||
// Handle form error
|
||||
if ($type === 'response_form_error') {
|
||||
// Handle validation and not found errors
|
||||
if ($type === \App\RSpade\Core\Ajax\Ajax::ERROR_VALIDATION || $type === \App\RSpade\Core\Ajax\Ajax::ERROR_NOT_FOUND) {
|
||||
// Only redirect if this was a POST request
|
||||
if (request()->isMethod('POST')) {
|
||||
Rsx::flash_error($reason);
|
||||
|
||||
@@ -7,6 +7,16 @@
|
||||
* Batches up to 20 calls or flushes after setTimeout(0) debounce.
|
||||
*/
|
||||
class Ajax {
|
||||
// Error code constants (must match server-side Ajax::ERROR_* constants)
|
||||
static ERROR_VALIDATION = 'validation';
|
||||
static ERROR_NOT_FOUND = 'not_found';
|
||||
static ERROR_UNAUTHORIZED = 'unauthorized';
|
||||
static ERROR_AUTH_REQUIRED = 'auth_required';
|
||||
static ERROR_FATAL = 'fatal';
|
||||
static ERROR_GENERIC = 'generic';
|
||||
static ERROR_SERVER = 'server_error'; // Client-generated (HTTP 500)
|
||||
static ERROR_NETWORK = 'network_error'; // Client-generated (connection failed)
|
||||
|
||||
/**
|
||||
* Initialize Ajax system
|
||||
* Called automatically when class is loaded
|
||||
@@ -198,81 +208,70 @@ class Ajax {
|
||||
resolve(processed_value);
|
||||
} else {
|
||||
// Handle error responses
|
||||
const error_type = response.error_type || 'unknown_error';
|
||||
const reason = response.reason || 'Unknown error occurred';
|
||||
const details = response.details || {};
|
||||
const error_code = response.error_code || Ajax.ERROR_GENERIC;
|
||||
const reason = response.reason || 'An error occurred';
|
||||
const metadata = response.metadata || {};
|
||||
|
||||
// Handle specific error types
|
||||
switch (error_type) {
|
||||
case 'fatal':
|
||||
// Fatal PHP error with full error details
|
||||
// Create error object
|
||||
const error = new Error(reason);
|
||||
error.code = error_code;
|
||||
error.metadata = metadata;
|
||||
|
||||
// Handle fatal errors specially
|
||||
if (error_code === Ajax.ERROR_FATAL) {
|
||||
const fatal_error_data = response.error || {};
|
||||
const error_message = fatal_error_data.error || 'Fatal error occurred';
|
||||
error.message = fatal_error_data.error || 'Fatal error occurred';
|
||||
error.metadata = response.error;
|
||||
|
||||
console.error('Ajax error response from server:', response.error);
|
||||
|
||||
const fatal_error = new Error(error_message);
|
||||
fatal_error.type = 'fatal';
|
||||
fatal_error.details = response.error;
|
||||
|
||||
// Log to server if browser error logging is enabled
|
||||
// Log to server
|
||||
Debugger.log_error({
|
||||
message: `Ajax Fatal Error: ${error_message}`,
|
||||
message: `Ajax Fatal Error: ${error.message}`,
|
||||
type: 'ajax_fatal',
|
||||
endpoint: url,
|
||||
details: response.error,
|
||||
});
|
||||
|
||||
reject(fatal_error);
|
||||
break;
|
||||
|
||||
case 'response_auth_required':
|
||||
console.error(
|
||||
'The user is no longer authenticated, this is a placeholder for future code which handles this scenario.'
|
||||
);
|
||||
const auth_error = new Error(reason);
|
||||
auth_error.type = 'auth_required';
|
||||
auth_error.details = details;
|
||||
reject(auth_error);
|
||||
break;
|
||||
|
||||
case 'response_unauthorized':
|
||||
console.error(
|
||||
'The user is unauthorized to perform this action, this is a placeholder for future code which handles this scenario.'
|
||||
);
|
||||
const unauth_error = new Error(reason);
|
||||
unauth_error.type = 'unauthorized';
|
||||
unauth_error.details = details;
|
||||
reject(unauth_error);
|
||||
break;
|
||||
|
||||
case 'response_form_error':
|
||||
const form_error = new Error(reason);
|
||||
form_error.type = 'form_error';
|
||||
form_error.details = details;
|
||||
reject(form_error);
|
||||
break;
|
||||
|
||||
default:
|
||||
const generic_error = new Error(reason);
|
||||
generic_error.type = error_type;
|
||||
generic_error.details = details;
|
||||
reject(generic_error);
|
||||
break;
|
||||
}
|
||||
|
||||
// Log auth errors for debugging
|
||||
if (error_code === Ajax.ERROR_AUTH_REQUIRED) {
|
||||
console.error('User is no longer authenticated');
|
||||
}
|
||||
if (error_code === Ajax.ERROR_UNAUTHORIZED) {
|
||||
console.error('User is unauthorized to perform this action');
|
||||
}
|
||||
|
||||
reject(error);
|
||||
}
|
||||
},
|
||||
error: (xhr, status, error) => {
|
||||
const error_message = Ajax._extract_error_message(xhr);
|
||||
const network_error = new Error(error_message);
|
||||
network_error.type = 'network_error';
|
||||
network_error.status = xhr.status;
|
||||
network_error.statusText = status;
|
||||
const err = new Error();
|
||||
|
||||
// Log server errors (500+) to the server if browser error logging is enabled
|
||||
// Determine error code based on status
|
||||
if (xhr.status >= 500) {
|
||||
// Server error (PHP crashed)
|
||||
err.code = Ajax.ERROR_SERVER;
|
||||
err.message = 'A server error occurred. Please try again.';
|
||||
} else if (xhr.status === 0 || status === 'timeout' || status === 'error') {
|
||||
// Network error (connection failed)
|
||||
err.code = Ajax.ERROR_NETWORK;
|
||||
err.message = 'Could not connect to server. Please check your connection.';
|
||||
} else {
|
||||
// Generic error
|
||||
err.code = Ajax.ERROR_GENERIC;
|
||||
err.message = Ajax._extract_error_message(xhr);
|
||||
}
|
||||
|
||||
err.metadata = {
|
||||
status: xhr.status,
|
||||
statusText: status
|
||||
};
|
||||
|
||||
// Log server errors to server
|
||||
if (xhr.status >= 500) {
|
||||
Debugger.log_error({
|
||||
message: `Ajax Server Error ${xhr.status}: ${error_message}`,
|
||||
message: `Ajax Server Error ${xhr.status}: ${err.message}`,
|
||||
type: 'ajax_server_error',
|
||||
endpoint: url,
|
||||
status: xhr.status,
|
||||
@@ -280,7 +279,7 @@ class Ajax {
|
||||
});
|
||||
}
|
||||
|
||||
reject(network_error);
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -376,26 +375,13 @@ class Ajax {
|
||||
});
|
||||
} else {
|
||||
// Handle error
|
||||
const error_type = call_response.error_type || 'unknown_error';
|
||||
let error_message;
|
||||
let error_details;
|
||||
|
||||
if (error_type === 'fatal' && call_response.error) {
|
||||
// Fatal PHP error with full error details
|
||||
const fatal_error_data = call_response.error;
|
||||
error_message = fatal_error_data.error || 'Fatal error occurred';
|
||||
error_details = call_response.error;
|
||||
|
||||
console.error('Ajax error response from server:', call_response.error);
|
||||
} else {
|
||||
// Other error types
|
||||
error_message = call_response.reason || 'Unknown error occurred';
|
||||
error_details = call_response.details || {};
|
||||
}
|
||||
const error_code = call_response.error_code || Ajax.ERROR_GENERIC;
|
||||
const error_message = call_response.reason || 'Unknown error occurred';
|
||||
const metadata = call_response.metadata || {};
|
||||
|
||||
const error = new Error(error_message);
|
||||
error.type = error_type;
|
||||
error.details = error_details;
|
||||
error.code = error_code;
|
||||
error.metadata = metadata;
|
||||
|
||||
pending_call.is_error = true;
|
||||
pending_call.error = error;
|
||||
@@ -410,7 +396,8 @@ class Ajax {
|
||||
// Network or server error - reject all pending calls
|
||||
const error_message = Ajax._extract_error_message(xhr_error);
|
||||
const error = new Error(error_message);
|
||||
error.type = 'network_error';
|
||||
error.code = Ajax.ERROR_NETWORK;
|
||||
error.metadata = {};
|
||||
|
||||
for (const call_id in call_map) {
|
||||
const pending_call = call_map[call_id];
|
||||
|
||||
@@ -30,8 +30,39 @@ class Debugger {
|
||||
// Check if browser error logging is enabled
|
||||
if (window.rsxapp && window.rsxapp.log_browser_errors) {
|
||||
// Listen for unhandled exceptions from Rsx event system
|
||||
Rsx.on('unhandled_exception', function (error_data) {
|
||||
Debugger._handle_browser_error(error_data);
|
||||
Rsx.on('unhandled_exception', function (payload) {
|
||||
// Extract exception from payload
|
||||
const exception = payload.exception;
|
||||
|
||||
// Normalize exception to error data object
|
||||
// Contract: exception can be Error object or string
|
||||
let errorData = {};
|
||||
|
||||
if (exception instanceof Error) {
|
||||
// Extract properties from Error object
|
||||
errorData.message = exception.message;
|
||||
errorData.stack = exception.stack;
|
||||
errorData.filename = exception.filename;
|
||||
errorData.lineno = exception.lineno;
|
||||
errorData.colno = exception.colno;
|
||||
errorData.type = 'exception';
|
||||
} else if (typeof exception === 'string') {
|
||||
// Plain string message
|
||||
errorData.message = exception;
|
||||
errorData.type = 'manual';
|
||||
} else if (exception && typeof exception === 'object') {
|
||||
// Object with message property (structured error)
|
||||
errorData = exception;
|
||||
if (!errorData.type) {
|
||||
errorData.type = 'manual';
|
||||
}
|
||||
} else {
|
||||
// Default case for unknown types
|
||||
errorData.message = String(exception);
|
||||
errorData.type = 'unknown';
|
||||
}
|
||||
|
||||
Debugger._handle_browser_error(errorData);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
193
app/RSpade/Core/Js/Exception_Handler.js
Executable file
193
app/RSpade/Core/Js/Exception_Handler.js
Executable 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,9 +50,6 @@ class Rsx {
|
||||
// Gets set to true to interupt startup sequence
|
||||
static __stopped = false;
|
||||
|
||||
// Timestamp of last flash alert for unhandled exceptions (rate limiting)
|
||||
static _last_exception_flash = 0;
|
||||
|
||||
// Initialize event handlers storage
|
||||
static _init_events() {
|
||||
if (typeof Rsx._event_handlers === 'undefined') {
|
||||
@@ -113,70 +110,101 @@ class Rsx {
|
||||
/**
|
||||
* Setup global unhandled exception handlers
|
||||
* Must be called before framework initialization begins
|
||||
*
|
||||
* Exception Event Contract:
|
||||
* -------------------------
|
||||
* When exceptions occur, they are broadcast via:
|
||||
* Rsx.trigger('unhandled_exception', { exception, meta })
|
||||
*
|
||||
* Event payload structure:
|
||||
* {
|
||||
* exception: Error|string, // The exception (Error object or string)
|
||||
* meta: {
|
||||
* source: 'window_error'|'unhandled_rejection'|undefined
|
||||
* // source = undefined means manually triggered (already logged by catcher)
|
||||
* // source = 'window_error' or 'unhandled_rejection' means needs logging
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* The exception can be:
|
||||
* 1. An Error object (preferred) - Has .message, .stack, .filename, .lineno properties
|
||||
* 2. A string - Plain error message text
|
||||
*
|
||||
* Consumers should handle both formats:
|
||||
* ```javascript
|
||||
* Rsx.on('unhandled_exception', function(payload) {
|
||||
* const exception = payload.exception;
|
||||
* const meta = payload.meta || {};
|
||||
*
|
||||
* let message;
|
||||
* if (exception instanceof Error) {
|
||||
* message = exception.message;
|
||||
* // Can also access: exception.stack, exception.filename, exception.lineno
|
||||
* } else if (typeof exception === 'string') {
|
||||
* message = exception;
|
||||
* } else {
|
||||
* message = String(exception);
|
||||
* }
|
||||
*
|
||||
* // Only log if from global handler (not already logged)
|
||||
* if (meta.source === 'window_error' || meta.source === 'unhandled_rejection') {
|
||||
* console.error('[Handler]', exception);
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* Exception Flow:
|
||||
* - window error/unhandledrejection → _handle_unhandled_exception(exception, {source})
|
||||
* - Triggers 'unhandled_exception' event with exception and metadata
|
||||
* - Debugger.js: Logs to server
|
||||
* - Exception_Handler: Logs to console (if from global handler) and displays
|
||||
* - Disables SPA navigation
|
||||
*/
|
||||
static _setup_exception_handlers() {
|
||||
// Handle uncaught JavaScript errors
|
||||
window.addEventListener('error', function (event) {
|
||||
Rsx._handle_unhandled_exception({
|
||||
message: event.message,
|
||||
filename: event.filename,
|
||||
lineno: event.lineno,
|
||||
colno: event.colno,
|
||||
stack: event.error ? event.error.stack : null,
|
||||
type: 'error',
|
||||
error: event.error,
|
||||
});
|
||||
// Pass the Error object directly if available, otherwise create one
|
||||
const exception = event.error || new Error(event.message);
|
||||
// Attach additional metadata if not already present
|
||||
if (!exception.filename) exception.filename = event.filename;
|
||||
if (!exception.lineno) exception.lineno = event.lineno;
|
||||
if (!exception.colno) exception.colno = event.colno;
|
||||
|
||||
Rsx._handle_unhandled_exception(exception, { source: 'window_error' });
|
||||
});
|
||||
|
||||
// Handle unhandled promise rejections
|
||||
window.addEventListener('unhandledrejection', function (event) {
|
||||
Rsx._handle_unhandled_exception({
|
||||
message: event.reason ? event.reason.message || String(event.reason) : 'Unhandled promise rejection',
|
||||
stack: event.reason && event.reason.stack ? event.reason.stack : null,
|
||||
type: 'unhandledrejection',
|
||||
error: event.reason,
|
||||
});
|
||||
// event.reason can be Error, string, or any value
|
||||
const exception = event.reason instanceof Error
|
||||
? event.reason
|
||||
: new Error(event.reason ? String(event.reason) : 'Unhandled promise rejection');
|
||||
|
||||
Rsx._handle_unhandled_exception(exception, { source: 'unhandled_rejection' });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal handler for unhandled exceptions
|
||||
* Triggers event, shows flash alert (rate limited), disables SPA, and logs to console
|
||||
* Triggers event and disables SPA
|
||||
* Display and logging handled by Exception_Handler listening to the event
|
||||
*
|
||||
* @param {Error|string|Object} exception - Exception object, string, or object with message
|
||||
* @param {Object} meta - Metadata about exception source
|
||||
* @param {string} meta.source - 'window_error', 'unhandled_rejection', or undefined for manual triggers
|
||||
*/
|
||||
static _handle_unhandled_exception(error_data) {
|
||||
// Always log to console
|
||||
console.error('[Rsx] Unhandled exception:', error_data);
|
||||
|
||||
// Trigger event for listeners (e.g., Debugger for server logging)
|
||||
Rsx.trigger('unhandled_exception', error_data);
|
||||
static _handle_unhandled_exception(exception, meta = {}) {
|
||||
// Trigger event for listeners:
|
||||
// - Debugger.js: Logs to server
|
||||
// - Exception_Handler: Logs to console and displays error (layout or flash alert)
|
||||
// Pass exception and metadata (source info for logging decisions)
|
||||
Rsx.trigger('unhandled_exception', { exception, meta });
|
||||
|
||||
// Disable SPA navigation if in SPA mode
|
||||
// This allows user to navigate away from broken page using normal browser navigation
|
||||
if (typeof Spa !== 'undefined' && window.rsxapp && window.rsxapp.is_spa) {
|
||||
Spa.disable();
|
||||
}
|
||||
|
||||
// Show flash alert (rate limited to 1 per second)
|
||||
const now = Date.now();
|
||||
if (now - Rsx._last_exception_flash >= 1000) {
|
||||
Rsx._last_exception_flash = now;
|
||||
|
||||
// Determine message based on dev/prod mode
|
||||
let message;
|
||||
if (window.rsxapp && window.rsxapp.debug) {
|
||||
// Dev mode: Show actual error (shortened to 300 chars)
|
||||
const error_text = error_data.message || 'Unknown error';
|
||||
message = error_text.length > 300 ? error_text.substring(0, 300) + '...' : error_text;
|
||||
} else {
|
||||
// Production mode: Generic message
|
||||
message = 'An unhandled error has occurred, you may need to refresh your page';
|
||||
}
|
||||
|
||||
// Show flash alert if Flash_Alert is available
|
||||
if (typeof Flash_Alert !== 'undefined') {
|
||||
Flash_Alert.error(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log to server that an event happened
|
||||
@@ -317,7 +345,19 @@ class Rsx {
|
||||
// Check if route exists in PHP controller definitions
|
||||
let pattern;
|
||||
if (Rsx._routes[class_name] && Rsx._routes[class_name][action_name]) {
|
||||
pattern = Rsx._routes[class_name][action_name];
|
||||
const route_patterns = Rsx._routes[class_name][action_name];
|
||||
|
||||
// Route patterns are always arrays (even for single routes)
|
||||
pattern = Rsx._select_best_route_pattern(route_patterns, params_obj);
|
||||
|
||||
if (!pattern) {
|
||||
// Route exists but no pattern satisfies the provided parameters
|
||||
const route_list = route_patterns.join(', ');
|
||||
throw new Error(
|
||||
`No suitable route found for ${class_name}::${action_name} with provided parameters. ` +
|
||||
`Available routes: ${route_list}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Not found in PHP routes - check if it's a SPA action
|
||||
pattern = Rsx._try_spa_action_route(class_name, params_obj);
|
||||
|
||||
@@ -2647,6 +2647,25 @@ class Manifest
|
||||
$method_data['parameters'] = $parameters;
|
||||
}
|
||||
|
||||
// Extract return type if available
|
||||
$return_type = $method->getReturnType();
|
||||
if ($return_type !== null) {
|
||||
if ($return_type instanceof \ReflectionUnionType) {
|
||||
// Union type (e.g., "array|null")
|
||||
$method_data['return_type'] = [
|
||||
'type' => 'union',
|
||||
'types' => array_map(fn($t) => $t->getName(), $return_type->getTypes()),
|
||||
'nullable' => $return_type->allowsNull()
|
||||
];
|
||||
} else {
|
||||
// Single type (e.g., "array", "string", "int")
|
||||
$method_data['return_type'] = [
|
||||
'type' => $return_type->getName(),
|
||||
'nullable' => $return_type->allowsNull()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$public_static_methods[$method->getName()] = $method_data;
|
||||
}
|
||||
|
||||
@@ -3173,6 +3192,37 @@ class Manifest
|
||||
'A method must be either a Route, Ajax_Endpoint, OR Task, not multiple types.'
|
||||
);
|
||||
}
|
||||
|
||||
// Check Ajax_Endpoint methods don't have return types
|
||||
if ($has_ajax_endpoint && isset($method_info['return_type'])) {
|
||||
$class_name = $metadata['class'] ?? 'Unknown';
|
||||
$return_type_info = $method_info['return_type'];
|
||||
|
||||
// Format return type for error message
|
||||
if (isset($return_type_info['type']) && $return_type_info['type'] === 'union') {
|
||||
$type_display = implode('|', $return_type_info['types']);
|
||||
} else {
|
||||
$type_display = $return_type_info['type'] ?? 'unknown';
|
||||
if (!empty($return_type_info['nullable'])) {
|
||||
$type_display = '?' . $type_display;
|
||||
}
|
||||
}
|
||||
|
||||
throw new \RuntimeException(
|
||||
"Ajax endpoint has forbidden return type declaration: {$type_display}\n" .
|
||||
"Class: {$class_name}\n" .
|
||||
"Method: {$method_name}\n" .
|
||||
"File: {$file_path}\n\n" .
|
||||
"Ajax endpoints must NOT declare return types because they need flexibility to return:\n" .
|
||||
"- Array data (success case)\n" .
|
||||
"- Form_Error_Response (validation errors)\n" .
|
||||
"- Redirect_Response (redirects)\n" .
|
||||
"- Other response types as needed\n\n" .
|
||||
"Solution: Remove the return type declaration from this method.\n" .
|
||||
"Change: public static function {$method_name}(...): {$type_display}\n" .
|
||||
"To: public static function {$method_name}(...)\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
63
app/RSpade/Core/Response/Error_Response.php
Executable file
63
app/RSpade/Core/Response/Error_Response.php
Executable 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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,9 @@ class Spa {
|
||||
// Flag to track if SPA is enabled (can be disabled on errors or dirty forms)
|
||||
static _spa_enabled = true;
|
||||
|
||||
// Timer ID for 30-minute auto-disable
|
||||
static _spa_timeout_timer = null;
|
||||
|
||||
/**
|
||||
* Disable SPA navigation - all navigation becomes full page loads
|
||||
* Call this when errors occur or forms are dirty
|
||||
@@ -55,6 +58,52 @@ class Spa {
|
||||
Spa._spa_enabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start 30-minute timeout to auto-disable SPA
|
||||
* Prevents users from working with stale code for more than 30 minutes
|
||||
*/
|
||||
static _start_spa_timeout() {
|
||||
// 30-minute timeout to auto-disable SPA navigation
|
||||
//
|
||||
// WHY: When the application is deployed with updated code, users who have the
|
||||
// SPA already loaded in their browser will continue using the old JavaScript
|
||||
// bundle indefinitely. This can cause:
|
||||
// - API mismatches (stale client code calling updated server endpoints)
|
||||
// - Missing features or UI changes
|
||||
// - Bugs from stale client-side logic
|
||||
//
|
||||
// FUTURE: A future version of RSpade will use WebSockets to trigger all clients
|
||||
// to automatically reload their pages on deploy. However, this timeout serves as
|
||||
// a secondary line of defense against:
|
||||
// - Failures in the WebSocket notification system
|
||||
// - Memory leaks in long-running SPA sessions
|
||||
// - Other unforeseen issues that may arise
|
||||
// This ensures that users will eventually and periodically get a fresh state,
|
||||
// regardless of any other system failures.
|
||||
//
|
||||
// SOLUTION: After 30 minutes, automatically disable SPA navigation. The next
|
||||
// forward navigation (link click, manual dispatch) will do a full page reload,
|
||||
// fetching the new bundle. Back/forward buttons continue to work via SPA
|
||||
// (force: true) to preserve form state and scroll position.
|
||||
//
|
||||
// 30 MINUTES: Chosen as a balance between:
|
||||
// - Short enough that users don't work with stale code for too long
|
||||
// - Long enough that users aren't interrupted during active work sessions
|
||||
//
|
||||
// TODO: Make this timeout value configurable by developers via:
|
||||
// - window.rsxapp.spa_timeout_minutes (set in PHP)
|
||||
// - Default to 30 if not specified
|
||||
// - Allow 0 to disable timeout entirely (for dev/testing)
|
||||
const timeout_ms = 30 * 60 * 1000;
|
||||
|
||||
Spa._spa_timeout_timer = setTimeout(() => {
|
||||
console.warn('[Spa] 30-minute timeout reached - disabling SPA navigation');
|
||||
Spa.disable();
|
||||
}, timeout_ms);
|
||||
|
||||
console_debug('Spa', '30-minute auto-disable timer started');
|
||||
}
|
||||
|
||||
/**
|
||||
* Framework module initialization hook called during framework boot
|
||||
* Only runs when window.rsxapp.is_spa === true
|
||||
@@ -67,6 +116,9 @@ class Spa {
|
||||
|
||||
console_debug('Spa', 'Initializing Spa system');
|
||||
|
||||
// Start 30-minute auto-disable timer
|
||||
Spa._start_spa_timeout();
|
||||
|
||||
// Discover and register all action classes
|
||||
Spa.discover_actions();
|
||||
|
||||
@@ -324,13 +376,6 @@ class Spa {
|
||||
// Get target URL (browser has already updated location)
|
||||
const url = window.location.pathname + window.location.search + window.location.hash;
|
||||
|
||||
// If SPA is disabled, still handle back/forward as SPA navigation
|
||||
// (We can't convert existing history entries to full page loads)
|
||||
// Only forward navigation (link clicks) will become full page loads
|
||||
if (!Spa._spa_enabled) {
|
||||
console_debug('Spa', 'SPA disabled but handling popstate as SPA navigation (back/forward)');
|
||||
}
|
||||
|
||||
// Retrieve scroll position from history state
|
||||
const scroll = e.state?.scroll || null;
|
||||
|
||||
@@ -349,9 +394,11 @@ class Spa {
|
||||
// const form_data = e.state?.form_data || {};
|
||||
|
||||
// Dispatch without modifying history (we're already at the target URL)
|
||||
// Force SPA dispatch even if disabled - popstate navigates to cached history state
|
||||
Spa.dispatch(url, {
|
||||
history: 'none',
|
||||
scroll: scroll
|
||||
scroll: scroll,
|
||||
force: true
|
||||
});
|
||||
});
|
||||
|
||||
@@ -433,10 +480,12 @@ class Spa {
|
||||
* - 'none': Don't modify history (used for back/forward)
|
||||
* @param {object|null} options.scroll - Scroll position {x, y} to restore (default: null = scroll to top)
|
||||
* @param {boolean} options.triggers - Fire before/after dispatch events (default: true)
|
||||
* @param {boolean} options.force - Force SPA dispatch even if disabled (used by popstate) (default: false)
|
||||
*/
|
||||
static async dispatch(url, options = {}) {
|
||||
// Check if SPA is disabled - do full page load
|
||||
if (!Spa._spa_enabled) {
|
||||
// Exception: popstate events always attempt SPA dispatch (force: true)
|
||||
if (!Spa._spa_enabled && !options.force) {
|
||||
console.warn('[Spa.dispatch] SPA disabled, forcing full page load');
|
||||
document.location.href = url;
|
||||
return;
|
||||
@@ -621,12 +670,18 @@ class Spa {
|
||||
Spa.layout.stop();
|
||||
}
|
||||
|
||||
// Clear body and create new layout
|
||||
$('body').empty();
|
||||
$('body').attr('class', '');
|
||||
// Clear spa-root and create new layout
|
||||
// Note: We target #spa-root instead of body to preserve global UI containers
|
||||
// (Flash_Alert, modals, tooltips, etc. that append to body)
|
||||
const $spa_root = $('#spa-root');
|
||||
if (!$spa_root.length) {
|
||||
throw new Error('[Spa] #spa-root element not found - check Spa_App.blade.php');
|
||||
}
|
||||
$spa_root.empty();
|
||||
$spa_root.attr('class', '');
|
||||
|
||||
// Create layout using component system
|
||||
Spa.layout = $('body').component(layout_name, {}).component();
|
||||
Spa.layout = $spa_root.component(layout_name, {}).component();
|
||||
|
||||
// Wait for layout to be ready
|
||||
await Spa.layout.ready();
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<meta content="ie=edge" http-equiv="X-UA-Compatible">
|
||||
|
||||
{{-- Bundle includes --}}
|
||||
{!! Frontend_Bundle::render() !!}
|
||||
{{-- Bundle includes - dynamically rendered based on SPA controller --}}
|
||||
{!! $bundle::render() !!}
|
||||
</head>
|
||||
|
||||
<body class="{{ rsx_body_class() }}">
|
||||
|
||||
<div id="spa-root"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -26,7 +26,53 @@ class Spa_Layout extends Component {
|
||||
* @returns {jQuery} The content element
|
||||
*/
|
||||
$content() {
|
||||
return this.$id('content');
|
||||
return this.$sid('content');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a debug exception message at the top of the content area
|
||||
* Prepends a styled error box with the exception message
|
||||
*
|
||||
* @param {string|Error} exception - Error message or Error object to display
|
||||
*/
|
||||
show_debug_exception(exception) {
|
||||
const $content = this.$content();
|
||||
if (!$content || !$content.length) {
|
||||
console.error('[Spa_Layout] Cannot show debug exception: content element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract message from Error object or use string directly
|
||||
let message;
|
||||
if (exception instanceof Error) {
|
||||
message = exception.message;
|
||||
} else if (typeof exception === 'string') {
|
||||
message = exception;
|
||||
} else if (exception && typeof exception === 'object' && exception.message) {
|
||||
message = exception.message;
|
||||
} else {
|
||||
message = String(exception);
|
||||
}
|
||||
|
||||
// Create error box with inline styles (no framework dependencies)
|
||||
const error_html = `
|
||||
<div style="border: 2px solid #dc3545; background-color: #ffe6e6; color: #000; padding: 15px; margin-bottom: 20px;">
|
||||
<strong>Fatal Error:</strong> ${this._escape_html(message)}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Prepend to content area
|
||||
$content.prepend(error_html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape HTML to prevent XSS
|
||||
* @private
|
||||
*/
|
||||
_escape_html(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,6 +123,9 @@ class Spa_Layout extends Component {
|
||||
// Clear content area
|
||||
$content.empty();
|
||||
|
||||
// Mark action as loading for exception display logic
|
||||
this._action_is_loading = true;
|
||||
|
||||
try {
|
||||
// Get the action class to check for @title decorator
|
||||
const action_class = Manifest.get_class_by_name(action_name);
|
||||
@@ -110,20 +159,15 @@ class Spa_Layout extends Component {
|
||||
|
||||
// Wait for action to be ready
|
||||
await action.ready();
|
||||
} catch (error) {
|
||||
// Action lifecycle failed - log error, trigger event, disable SPA, show error UI
|
||||
console.error('[Spa_Layout] Action lifecycle failed:', error);
|
||||
|
||||
// Trigger global exception event (goes to Debugger for server logging, Flash_Alert, etc)
|
||||
if (typeof Rsx !== 'undefined') {
|
||||
Rsx.trigger('unhandled_exception', {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
type: 'action_lifecycle_error',
|
||||
action_name: action_name,
|
||||
error: error,
|
||||
});
|
||||
}
|
||||
// Mark action as done loading
|
||||
this._action_is_loading = false;
|
||||
} catch (error) {
|
||||
// Mark action as done loading (even though it failed)
|
||||
this._action_is_loading = false;
|
||||
|
||||
// Action lifecycle failed - log and trigger event
|
||||
console.error('[Spa_Layout] Action lifecycle failed:', error);
|
||||
|
||||
// Disable SPA so forward navigation becomes full page loads
|
||||
// (Back/forward still work as SPA to allow user to navigate away)
|
||||
@@ -131,16 +175,12 @@ class Spa_Layout extends Component {
|
||||
Spa.disable();
|
||||
}
|
||||
|
||||
// Show error UI in content area so user can navigate away
|
||||
$content.html(`
|
||||
<div class="alert alert-danger m-4">
|
||||
<h4>Page Failed to Load</h4>
|
||||
<p>An error occurred while loading this page. You can navigate back or to another page.</p>
|
||||
<p class="mb-0">
|
||||
<a href="javascript:history.back()" class="btn btn-secondary">Go Back</a>
|
||||
</p>
|
||||
</div>
|
||||
`);
|
||||
// Trigger global exception event
|
||||
// Exception_Handler will decide how to display (layout or flash alert)
|
||||
// Pass payload with exception (no meta.source = already logged above)
|
||||
if (typeof Rsx !== 'undefined') {
|
||||
Rsx.trigger('unhandled_exception', { exception: error, meta: {} });
|
||||
}
|
||||
|
||||
// Don't re-throw - allow navigation to continue working
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
|
||||
* Generate random first name
|
||||
*/
|
||||
#[Ajax_Endpoint]
|
||||
public static function first_name(Request $request, array $params = []): string
|
||||
public static function first_name(Request $request, array $params = [])
|
||||
{
|
||||
self::__check_production();
|
||||
return self::__random_from_list('first_names');
|
||||
@@ -77,7 +77,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
|
||||
* Generate random last name
|
||||
*/
|
||||
#[Ajax_Endpoint]
|
||||
public static function last_name(Request $request, array $params = []): string
|
||||
public static function last_name(Request $request, array $params = [])
|
||||
{
|
||||
self::__check_production();
|
||||
return self::__random_from_list('last_names');
|
||||
@@ -87,7 +87,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
|
||||
* Generate random company name
|
||||
*/
|
||||
#[Ajax_Endpoint]
|
||||
public static function company_name(Request $request, array $params = []): string
|
||||
public static function company_name(Request $request, array $params = [])
|
||||
{
|
||||
self::__check_production();
|
||||
|
||||
@@ -109,7 +109,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
|
||||
* Generate random street address
|
||||
*/
|
||||
#[Ajax_Endpoint]
|
||||
public static function address(Request $request, array $params = []): string
|
||||
public static function address(Request $request, array $params = [])
|
||||
{
|
||||
self::__check_production();
|
||||
|
||||
@@ -124,7 +124,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
|
||||
* Generate random city name
|
||||
*/
|
||||
#[Ajax_Endpoint]
|
||||
public static function city(Request $request, array $params = []): string
|
||||
public static function city(Request $request, array $params = [])
|
||||
{
|
||||
self::__check_production();
|
||||
return self::__random_from_list('cities');
|
||||
@@ -134,7 +134,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
|
||||
* Generate random US state code
|
||||
*/
|
||||
#[Ajax_Endpoint]
|
||||
public static function state(Request $request, array $params = []): string
|
||||
public static function state(Request $request, array $params = [])
|
||||
{
|
||||
self::__check_production();
|
||||
|
||||
@@ -153,7 +153,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
|
||||
* Generate random ZIP code
|
||||
*/
|
||||
#[Ajax_Endpoint]
|
||||
public static function zip(Request $request, array $params = []): string
|
||||
public static function zip(Request $request, array $params = [])
|
||||
{
|
||||
self::__check_production();
|
||||
return str_pad((string)rand(10000, 99999), 5, '0', STR_PAD_LEFT);
|
||||
@@ -164,7 +164,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
|
||||
* Avoids 555-000-0000 and any sequence containing 911
|
||||
*/
|
||||
#[Ajax_Endpoint]
|
||||
public static function phone(Request $request, array $params = []): string
|
||||
public static function phone(Request $request, array $params = [])
|
||||
{
|
||||
self::__check_production();
|
||||
|
||||
@@ -193,7 +193,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
|
||||
* Format: wordwordnumbers+test@gmail.com
|
||||
*/
|
||||
#[Ajax_Endpoint]
|
||||
public static function email(Request $request, array $params = []): string
|
||||
public static function email(Request $request, array $params = [])
|
||||
{
|
||||
self::__check_production();
|
||||
|
||||
@@ -208,7 +208,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
|
||||
* Generate random website URL
|
||||
*/
|
||||
#[Ajax_Endpoint]
|
||||
public static function website(Request $request, array $params = []): string
|
||||
public static function website(Request $request, array $params = [])
|
||||
{
|
||||
self::__check_production();
|
||||
|
||||
@@ -223,7 +223,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
|
||||
* Generate random text/paragraph
|
||||
*/
|
||||
#[Ajax_Endpoint]
|
||||
public static function text(Request $request, array $params = []): string
|
||||
public static function text(Request $request, array $params = [])
|
||||
{
|
||||
self::__check_production();
|
||||
|
||||
|
||||
@@ -1060,66 +1060,15 @@ function rsx_body_class()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an authentication required response
|
||||
* Create a unified error response
|
||||
*
|
||||
* Returns a special response object that Dispatch and Ajax_Endpoint handle differently:
|
||||
* - HTTP requests: Sets flash alert and redirects to login
|
||||
* - AJAX requests: Returns JSON error with success:false
|
||||
*
|
||||
* @param string $reason The reason authentication is required
|
||||
* @param string|null $redirect The URL to redirect to (default: /login)
|
||||
* @return \App\RSpade\Core\Response\Auth_Required_Response
|
||||
* @param string $error_code One of Ajax::ERROR_* constants
|
||||
* @param string|array|null $metadata Error message (string) or structured data (array)
|
||||
* @return \App\RSpade\Core\Response\Error_Response
|
||||
*/
|
||||
function response_auth_required($reason = 'Authentication Required', $redirect = '/login')
|
||||
function response_error(string $error_code, $metadata = null)
|
||||
{
|
||||
return new \App\RSpade\Core\Response\Auth_Required_Response($reason, $redirect);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an unauthorized response
|
||||
*
|
||||
* Returns a special response object that Dispatch and Ajax_Endpoint handle differently:
|
||||
* - HTTP requests: Sets flash alert and redirects or throws exception
|
||||
* - AJAX requests: Returns JSON error with success:false
|
||||
*
|
||||
* @param string $reason The reason for unauthorized access
|
||||
* @param string|null $redirect The URL to redirect to (null throws exception)
|
||||
* @return \App\RSpade\Core\Response\Unauthorized_Response
|
||||
*/
|
||||
function response_unauthorized($reason = 'Unauthorized', $redirect = null)
|
||||
{
|
||||
return new \App\RSpade\Core\Response\Unauthorized_Response($reason, $redirect);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a form error response
|
||||
*
|
||||
* Returns a special response object that Dispatch and Ajax_Endpoint handle differently:
|
||||
* - HTTP requests: Sets flash alert and redirects back to same URL as GET
|
||||
* - AJAX requests: Returns JSON error with success:false and details
|
||||
*
|
||||
* @param string $reason The error message
|
||||
* @param array $details Additional error details
|
||||
* @return \App\RSpade\Core\Response\Form_Error_Response
|
||||
*/
|
||||
function response_form_error($reason = 'An error has occurred.', $details = [])
|
||||
{
|
||||
return new \App\RSpade\Core\Response\Form_Error_Response($reason, $details);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a fatal error response
|
||||
*
|
||||
* Returns a special response object that always throws an exception
|
||||
* in both HTTP and AJAX contexts
|
||||
*
|
||||
* @param string $reason The error message
|
||||
* @param array $details Additional error details
|
||||
* @return \App\RSpade\Core\Response\Fatal_Error_Response
|
||||
*/
|
||||
function response_fatal_error($reason = 'An error has occurred.', $details = [])
|
||||
{
|
||||
return new \App\RSpade\Core\Response\Fatal_Error_Response($reason, $details);
|
||||
return new \App\RSpade\Core\Response\Error_Response($error_code, $metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,7 +20,7 @@ SYNOPSIS
|
||||
|
||||
on_ready() {
|
||||
// All children ready, safe for DOM manipulation
|
||||
this.$id('edit').on('click', () => this.edit());
|
||||
this.$sid('edit').on('click', () => this.edit());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,10 +83,10 @@ TEMPLATE SYNTAX
|
||||
|
||||
<Define:User_Card>
|
||||
<div class="card">
|
||||
<img $id="avatar" src="<%= this.data.avatar %>" />
|
||||
<h3 $id="name"><%= this.data.name %></h3>
|
||||
<p $id="email"><%= this.data.email %></p>
|
||||
<button $id="edit">Edit</button>
|
||||
<img $sid="avatar" src="<%= this.data.avatar %>" />
|
||||
<h3 $sid="name"><%= this.data.name %></h3>
|
||||
<p $sid="email"><%= this.data.email %></p>
|
||||
<button $sid="edit">Edit</button>
|
||||
</div>
|
||||
</Define:User_Card>
|
||||
|
||||
@@ -94,9 +94,10 @@ TEMPLATE SYNTAX
|
||||
<%= expression %> - Escaped HTML output (safe, default)
|
||||
<%!= expression %> - Unescaped raw output (pre-sanitized content only)
|
||||
<% statement; %> - JavaScript statements (loops, conditionals)
|
||||
<%-- comment --%> - JQHTML comments (not HTML <!-- --> comments)
|
||||
|
||||
Attributes:
|
||||
$id="name" - Scoped ID (becomes id="name:component_id")
|
||||
$sid="name" - Scoped ID (becomes id="name:component_id")
|
||||
$attr=value - Component parameter (becomes this.args.attr)
|
||||
Note: Also creates data-attr HTML attribute
|
||||
@event=this.method - Event binding (⚠️ verify functionality)
|
||||
@@ -255,7 +256,7 @@ THIS.ARGS VS THIS.DATA
|
||||
Lifecycle Restrictions (ENFORCED):
|
||||
- on_create(): Can modify this.data (set defaults)
|
||||
- on_load(): Can ONLY access this.args and this.data
|
||||
Cannot access this.$, this.$id(), or any other properties
|
||||
Cannot access this.$, this.$sid(), or any other properties
|
||||
Can modify this.data freely
|
||||
- on_ready() / event handlers: Can modify this.args, read this.data
|
||||
CANNOT modify this.data (frozen)
|
||||
@@ -286,7 +287,7 @@ THIS.ARGS VS THIS.DATA
|
||||
|
||||
on_ready() {
|
||||
// Modify state, then reload
|
||||
this.$id('filter_btn').on('click', () => {
|
||||
this.$sid('filter_btn').on('click', () => {
|
||||
this.args.filter = 'active'; // Change state
|
||||
this.reload(); // Re-fetch with new state
|
||||
});
|
||||
@@ -375,6 +376,50 @@ CONTROL FLOW AND LOOPS
|
||||
%>
|
||||
<p>Total: $<%= total.toFixed(2) %></p>
|
||||
|
||||
COMMENTS IN TEMPLATES
|
||||
JQHTML uses its own comment syntax, NOT HTML comments:
|
||||
|
||||
Correct - JQHTML comments (parser removes, never in output):
|
||||
<%--
|
||||
This is a JQHTML comment
|
||||
Completely removed during compilation
|
||||
Perfect for component documentation
|
||||
--%>
|
||||
|
||||
Incorrect - HTML comments (parser DOES NOT remove):
|
||||
<!--
|
||||
This is an HTML comment
|
||||
Parser treats this as literal HTML
|
||||
Will appear in rendered output
|
||||
Still processes JQHTML directives inside!
|
||||
-->
|
||||
|
||||
Critical difference:
|
||||
HTML comments <!-- --> do NOT block JQHTML directive execution.
|
||||
Code inside HTML comments will still execute, just like PHP code
|
||||
inside HTML comments in .php files still executes.
|
||||
|
||||
WRONG - This WILL execute:
|
||||
<!-- <% dangerous_code(); %> -->
|
||||
|
||||
CORRECT - This will NOT execute:
|
||||
<%-- <% safe_code(); %> --%>
|
||||
|
||||
Component docblocks:
|
||||
Use JQHTML comments at the top of component templates:
|
||||
|
||||
<%--
|
||||
User_Card_Component
|
||||
|
||||
Displays user profile information in a card layout.
|
||||
|
||||
$user_id - ID of user to display
|
||||
$show_avatar - Whether to show profile photo (default: true)
|
||||
--%>
|
||||
<Define:User_Card_Component>
|
||||
<!-- Component template here -->
|
||||
</Define:User_Card_Component>
|
||||
|
||||
COMPONENT LIFECYCLE
|
||||
Five-stage deterministic lifecycle:
|
||||
|
||||
@@ -403,7 +448,7 @@ COMPONENT LIFECYCLE
|
||||
4. on_load() (bottom-up, siblings in parallel, CAN be async)
|
||||
- Load async data based on this.args
|
||||
- ONLY access this.args and this.data (RESTRICTED)
|
||||
- CANNOT access this.$, this.$id(), or any other properties
|
||||
- CANNOT access this.$, this.$sid(), or any other properties
|
||||
- ONLY modify this.data - NEVER DOM
|
||||
- NO child component access
|
||||
- Siblings at same depth execute in parallel
|
||||
@@ -642,7 +687,7 @@ JAVASCRIPT COMPONENT CLASS
|
||||
on_ready() {
|
||||
// All children ready, safe for DOM
|
||||
// Attach event handlers
|
||||
this.$id('select_all').on('click', () => this.select_all());
|
||||
this.$sid('select_all').on('click', () => this.select_all());
|
||||
this.$.animate({opacity: 1}, 300);
|
||||
}
|
||||
|
||||
@@ -846,12 +891,12 @@ $REDRAWABLE ATTRIBUTE - LIGHTWEIGHT COMPONENTS
|
||||
converts the element into a <Redrawable> component:
|
||||
|
||||
<!-- Write this: -->
|
||||
<div $redrawable $id="counter">
|
||||
<div $redrawable $sid="counter">
|
||||
Count: <%= this.data.count %>
|
||||
</div>
|
||||
|
||||
<!-- Parser transforms to: -->
|
||||
<Redrawable data-tag="div" $id="counter">
|
||||
<Redrawable data-tag="div" $sid="counter">
|
||||
Count: <%= this.data.count %>
|
||||
</Redrawable>
|
||||
|
||||
@@ -867,12 +912,12 @@ $REDRAWABLE ATTRIBUTE - LIGHTWEIGHT COMPONENTS
|
||||
async increment_counter() {
|
||||
this.data.count++;
|
||||
// Re-render only the counter element, not entire dashboard
|
||||
this.render('counter'); // Finds child with $id="counter"
|
||||
this.render('counter'); // Finds child with $sid="counter"
|
||||
}
|
||||
}
|
||||
|
||||
render(id) Delegation Syntax:
|
||||
- this.render('counter') finds child with $id="counter"
|
||||
- this.render('counter') finds child with $sid="counter"
|
||||
- Verifies element is a component (has $redrawable or is proper
|
||||
component class)
|
||||
- Calls its render() method to update only that element
|
||||
@@ -881,7 +926,7 @@ $REDRAWABLE ATTRIBUTE - LIGHTWEIGHT COMPONENTS
|
||||
- Parent component's DOM remains unchanged
|
||||
|
||||
Error Handling:
|
||||
- Clear error if $id doesn't exist in children
|
||||
- Clear error if $sid doesn't exist in children
|
||||
- Clear error if element isn't configured as component
|
||||
- Guides developers to correct usage patterns
|
||||
|
||||
@@ -922,7 +967,7 @@ LIFECYCLE MANIPULATION METHODS
|
||||
}
|
||||
|
||||
on_ready() {
|
||||
this.$id('filter_btn').on('click', async () => {
|
||||
this.$sid('filter_btn').on('click', async () => {
|
||||
this.args.filter = 'active'; // Update state
|
||||
await this.reload(); // Re-fetch with new state
|
||||
});
|
||||
@@ -1047,26 +1092,26 @@ DOM UTILITIES
|
||||
jQuery wrapped component root element
|
||||
This is genuine jQuery - all methods work directly
|
||||
|
||||
this.$id(name)
|
||||
this.$sid(name)
|
||||
Get scoped element as jQuery object
|
||||
Example: this.$id('edit') gets element with $id="edit"
|
||||
Example: this.$sid('edit') gets element with $sid="edit"
|
||||
Returns jQuery object, NOT component instance
|
||||
|
||||
this.id(name)
|
||||
this.sid(name)
|
||||
Get scoped child component instance directly
|
||||
Example: this.id('my_component') gets component instance
|
||||
Example: this.sid('my_component') gets component instance
|
||||
Returns component instance, NOT jQuery object
|
||||
|
||||
CRITICAL: this.$id() vs this.id() distinction
|
||||
- this.$id('foo') → jQuery object (for DOM manipulation)
|
||||
- this.id('foo') → Component instance (for calling methods)
|
||||
CRITICAL: this.$sid() vs this.sid() distinction
|
||||
- this.$sid('foo') → jQuery object (for DOM manipulation)
|
||||
- this.sid('foo') → Component instance (for calling methods)
|
||||
|
||||
Common mistake:
|
||||
const comp = this.id('foo').component(); // ❌ WRONG
|
||||
const comp = this.id('foo'); // ✅ CORRECT
|
||||
const comp = this.sid('foo').component(); // ❌ WRONG
|
||||
const comp = this.sid('foo'); // ✅ CORRECT
|
||||
|
||||
Getting component from jQuery:
|
||||
const $elem = this.$id('foo');
|
||||
const $elem = this.$sid('foo');
|
||||
const comp = $elem.component(); // ✅ CORRECT (jQuery → component)
|
||||
|
||||
this.data
|
||||
@@ -1105,13 +1150,13 @@ NESTING COMPONENTS
|
||||
on_load, on_ready).
|
||||
|
||||
SCOPED IDS
|
||||
Use $id attribute for component-scoped element IDs:
|
||||
Use $sid attribute for component-scoped element IDs:
|
||||
|
||||
Template:
|
||||
<Define:User_Card>
|
||||
<h3 $id="title">Name</h3>
|
||||
<p $id="email">Email</p>
|
||||
<button $id="edit_btn">Edit</button>
|
||||
<h3 $sid="title">Name</h3>
|
||||
<p $sid="email">Email</p>
|
||||
<button $sid="edit_btn">Edit</button>
|
||||
</Define:User_Card>
|
||||
|
||||
Rendered HTML (automatic scoping):
|
||||
@@ -1121,13 +1166,13 @@ SCOPED IDS
|
||||
<button id="edit_btn:c123">Edit</button>
|
||||
</div>
|
||||
|
||||
Access with this.$id():
|
||||
Access with this.$sid():
|
||||
class User_Card extends Jqhtml_Component {
|
||||
on_ready() {
|
||||
// Use logical name
|
||||
this.$id('title').text('John Doe');
|
||||
this.$id('email').text('john@example.com');
|
||||
this.$id('edit_btn').on('click', () => this.edit());
|
||||
this.$sid('title').text('John Doe');
|
||||
this.$sid('email').text('john@example.com');
|
||||
this.$sid('edit_btn').on('click', () => this.edit());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1148,12 +1193,12 @@ EXAMPLES
|
||||
|
||||
on_ready() {
|
||||
// Attach event handlers after data loaded
|
||||
this.$id('buy').on('click', async () => {
|
||||
this.$sid('buy').on('click', async () => {
|
||||
await Cart.add(this.data.id);
|
||||
this.$id('buy').text('Added!').prop('disabled', true);
|
||||
this.$sid('buy').text('Added!').prop('disabled', true);
|
||||
});
|
||||
|
||||
this.$id('favorite').on('click', () => {
|
||||
this.$sid('favorite').on('click', () => {
|
||||
this.$.toggleClass('favorited');
|
||||
});
|
||||
}
|
||||
@@ -1208,19 +1253,19 @@ EXAMPLES
|
||||
}
|
||||
|
||||
validate() {
|
||||
const email = this.$id('email').val();
|
||||
const email = this.$sid('email').val();
|
||||
if (!email.includes('@')) {
|
||||
this.$id('error').text('Invalid email');
|
||||
this.$sid('error').text('Invalid email');
|
||||
return false;
|
||||
}
|
||||
this.$id('error').text('');
|
||||
this.$sid('error').text('');
|
||||
return true;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
const data = {
|
||||
email: this.$id('email').val(),
|
||||
message: this.$id('message').val(),
|
||||
email: this.$sid('email').val(),
|
||||
message: this.$sid('message').val(),
|
||||
};
|
||||
|
||||
await fetch('/contact', {
|
||||
@@ -1391,6 +1436,70 @@ CONTENT AND SLOTS
|
||||
- Form patterns with customizable field sets
|
||||
- Any component hierarchy with shared structure
|
||||
|
||||
TEMPLATE-ONLY COMPONENTS
|
||||
Components can exist as .jqhtml files without a companion .js file.
|
||||
This is fine for simple display-only components that just render
|
||||
input data with conditionals - no lifecycle hooks or event handlers
|
||||
needed beyond what's possible inline.
|
||||
|
||||
When to use template-only:
|
||||
- Component just displays data passed via arguments
|
||||
- Only needs simple conditionals in the template
|
||||
- No complex event handling beyond simple button clicks
|
||||
- Mentally easier than creating a separate .js file
|
||||
|
||||
Inline Event Handlers:
|
||||
Define handlers in template code, reference with @event syntax:
|
||||
|
||||
<Define:Retry_Button>
|
||||
<% this.handle_click = () => window.location.reload(); %>
|
||||
<button class="btn btn-primary" @click=this.handle_click>
|
||||
Retry
|
||||
</button>
|
||||
</Define:Retry_Button>
|
||||
|
||||
Note: @event values must be UNQUOTED (not @click="this.method").
|
||||
|
||||
Inline Argument Validation:
|
||||
Throw errors early if required arguments are missing:
|
||||
|
||||
<Define:User_Badge>
|
||||
<%
|
||||
if (!this.args.user_id) {
|
||||
throw new Error('User_Badge: $user_id is required');
|
||||
}
|
||||
if (!this.args.name) {
|
||||
throw new Error('User_Badge: $name is required');
|
||||
}
|
||||
%>
|
||||
<span class="badge"><%= this.args.name %></span>
|
||||
</Define:User_Badge>
|
||||
|
||||
Complete Example (error page component):
|
||||
<%--
|
||||
Not_Found_Error_Page_Component
|
||||
Displays when a record cannot be found.
|
||||
--%>
|
||||
<Define:Not_Found_Error_Page_Component class="text-center py-5">
|
||||
<div class="mb-4">
|
||||
<i class="bi bi-exclamation-triangle-fill text-warning"
|
||||
style="font-size: 4rem;"></i>
|
||||
</div>
|
||||
<h3 class="mb-3"><%= this.args.record_type %> Not Found</h3>
|
||||
<p class="text-muted mb-4">
|
||||
The <%= this.args.record_type.toLowerCase() %> you are
|
||||
looking for does not exist or has been deleted.
|
||||
</p>
|
||||
<a href="<%= this.args.back_url %>" class="btn btn-primary">
|
||||
<%= this.args.back_label %>
|
||||
</a>
|
||||
</Define:Not_Found_Error_Page_Component>
|
||||
|
||||
This pattern is not necessarily "best practice" for complex components,
|
||||
but it works well and is pragmatic for simple display components. If
|
||||
the component needs lifecycle hooks, state management, or complex
|
||||
event handling, create a companion .js file instead.
|
||||
|
||||
INTEGRATION WITH RSX
|
||||
JQHTML automatically integrates with the RSX framework:
|
||||
- Templates discovered by manifest system during build
|
||||
|
||||
230
app/RSpade/man/view_action_patterns.txt
Executable file
230
app/RSpade/man/view_action_patterns.txt
Executable file
@@ -0,0 +1,230 @@
|
||||
VIEW_ACTION_PATTERNS(3) RSX Manual VIEW_ACTION_PATTERNS(3)
|
||||
|
||||
NAME
|
||||
view_action_patterns - Best practices for SPA view actions with
|
||||
dynamic content loading
|
||||
|
||||
SYNOPSIS
|
||||
Recommended pattern for view/detail pages that load a single record:
|
||||
|
||||
Action Class (Feature_View_Action.js):
|
||||
@route('/feature/view/:id')
|
||||
@layout('Frontend_Spa_Layout')
|
||||
@spa('Frontend_Spa_Controller::index')
|
||||
@title('Feature Details')
|
||||
class Feature_View_Action extends Spa_Action {
|
||||
on_create() {
|
||||
this.data.record = { name: '' };
|
||||
this.data.error_data = null;
|
||||
this.data.loading = true;
|
||||
}
|
||||
|
||||
async on_load() {
|
||||
try {
|
||||
this.data.record = await Controller.get({
|
||||
id: this.args.id
|
||||
});
|
||||
} catch (e) {
|
||||
this.data.error_data = e;
|
||||
}
|
||||
this.data.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
Template (Feature_View_Action.jqhtml):
|
||||
<Define:Feature_View_Action>
|
||||
<Page>
|
||||
<% if (this.data.loading) { %>
|
||||
<Loading_Spinner $message="Loading..." />
|
||||
<% } else if (this.data.error_data) { %>
|
||||
<Universal_Error_Page_Component
|
||||
$error_data="<%= this.data.error_data %>"
|
||||
$record_type="Feature"
|
||||
$back_label="Go back to Features"
|
||||
$back_url="<%= Rsx.Route('Feature_Index_Action') %>"
|
||||
/>
|
||||
<% } else { %>
|
||||
<!-- Normal content here -->
|
||||
<% } %>
|
||||
</Page>
|
||||
</Define:Feature_View_Action>
|
||||
|
||||
DESCRIPTION
|
||||
This document describes the recommended pattern for building view/detail
|
||||
pages in RSpade SPA applications. The pattern provides:
|
||||
|
||||
- Loading state with spinner during data fetch
|
||||
- Automatic error handling for all Ajax error types
|
||||
- Clean three-state template (loading → error → content)
|
||||
- Consistent user experience across all view pages
|
||||
|
||||
This is an opinionated best practice from the RSpade starter template.
|
||||
Developers are free to implement alternatives, but this pattern handles
|
||||
common cases well and provides a consistent structure.
|
||||
|
||||
THE THREE-STATE PATTERN
|
||||
Every view action has exactly three possible states:
|
||||
|
||||
1. LOADING - Data is being fetched
|
||||
- Show Loading_Spinner component
|
||||
- Prevents flash of empty/broken content
|
||||
- User knows something is happening
|
||||
|
||||
2. ERROR - Data fetch failed
|
||||
- Show Universal_Error_Page_Component
|
||||
- Automatically routes to appropriate error display
|
||||
- Handles not found, unauthorized, server errors, etc.
|
||||
|
||||
3. SUCCESS - Data loaded successfully
|
||||
- Show normal page content
|
||||
- Safe to access this.data.record properties
|
||||
|
||||
Template structure:
|
||||
<% if (this.data.loading) { %>
|
||||
<!-- State 1: Loading -->
|
||||
<% } else if (this.data.error_data) { %>
|
||||
<!-- State 2: Error -->
|
||||
<% } else { %>
|
||||
<!-- State 3: Success -->
|
||||
<% } %>
|
||||
|
||||
ACTION CLASS STRUCTURE
|
||||
on_create() - Initialize defaults
|
||||
on_create() {
|
||||
// Stub object prevents undefined errors during first render
|
||||
this.data.record = { name: '' };
|
||||
|
||||
// Error holder - null means no error
|
||||
this.data.error_data = null;
|
||||
|
||||
// Start in loading state
|
||||
this.data.loading = true;
|
||||
}
|
||||
|
||||
Key points:
|
||||
- Initialize this.data.record with empty stub matching expected shape
|
||||
- Prevents "cannot read property of undefined" during initial render
|
||||
- Set loading=true so spinner shows immediately
|
||||
|
||||
async on_load() - Fetch data with error handling
|
||||
async on_load() {
|
||||
try {
|
||||
this.data.record = await Controller.get({
|
||||
id: this.args.id
|
||||
});
|
||||
} catch (e) {
|
||||
this.data.error_data = e;
|
||||
}
|
||||
this.data.loading = false;
|
||||
}
|
||||
|
||||
Key points:
|
||||
- Wrap Ajax call in try/catch
|
||||
- Store caught error in this.data.error_data (not re-throw)
|
||||
- Always set loading=false in finally or after try/catch
|
||||
- Error object has .code, .message, .metadata from Ajax system
|
||||
|
||||
UNIVERSAL ERROR COMPONENT
|
||||
The Universal_Error_Page_Component automatically displays the right
|
||||
error UI based on error.code:
|
||||
|
||||
Required arguments:
|
||||
$error_data - The error object from catch block
|
||||
$record_type - Human name: "Project", "Contact", "User"
|
||||
$back_label - Button text: "Go back to Projects"
|
||||
$back_url - Where back button navigates
|
||||
|
||||
Error types handled:
|
||||
Ajax.ERROR_NOT_FOUND → "Project Not Found" with back button
|
||||
Ajax.ERROR_UNAUTHORIZED → "Access Denied" message
|
||||
Ajax.ERROR_AUTH_REQUIRED → "Login Required" with login button
|
||||
Ajax.ERROR_SERVER → "Server Error" with retry button
|
||||
Ajax.ERROR_NETWORK → "Connection Error" with retry button
|
||||
Ajax.ERROR_VALIDATION → Field error list
|
||||
Ajax.ERROR_GENERIC → Generic error with retry
|
||||
|
||||
HANDLING SPECIFIC ERRORS DIFFERENTLY
|
||||
Sometimes you need custom handling for specific error types while
|
||||
letting others go to the universal handler:
|
||||
|
||||
async on_load() {
|
||||
try {
|
||||
this.data.record = await Controller.get({id: this.args.id});
|
||||
} catch (e) {
|
||||
if (e.code === Ajax.ERROR_NOT_FOUND) {
|
||||
// Custom handling: redirect to create page
|
||||
Spa.dispatch(Rsx.Route('Feature_Create_Action'));
|
||||
return;
|
||||
}
|
||||
// All other errors: use universal handler
|
||||
this.data.error_data = e;
|
||||
}
|
||||
this.data.loading = false;
|
||||
}
|
||||
|
||||
Or in the template for different error displays:
|
||||
|
||||
<% } else if (this.data.error_data) { %>
|
||||
<% if (this.data.error_data.code === Ajax.ERROR_NOT_FOUND) { %>
|
||||
<!-- Custom not-found UI -->
|
||||
<div class="text-center">
|
||||
<p>This project doesn't exist yet.</p>
|
||||
<a href="..." class="btn btn-primary">Create It</a>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<!-- Standard error handling -->
|
||||
<Universal_Error_Page_Component ... />
|
||||
<% } %>
|
||||
<% } else { %>
|
||||
|
||||
LOADING SPINNER
|
||||
The Loading_Spinner component provides consistent loading UI:
|
||||
|
||||
<Loading_Spinner />
|
||||
<Loading_Spinner $message="Loading project details..." />
|
||||
|
||||
Located at: rsx/theme/components/feedback/loading_spinner.jqhtml
|
||||
|
||||
COMPLETE EXAMPLE
|
||||
From rsx/app/frontend/projects/Projects_View_Action.js:
|
||||
|
||||
@route('/projects/view/:id')
|
||||
@layout('Frontend_Spa_Layout')
|
||||
@spa('Frontend_Spa_Controller::index')
|
||||
@title('Project Details')
|
||||
class Projects_View_Action extends Spa_Action {
|
||||
on_create() {
|
||||
this.data.project = { name: '' };
|
||||
this.data.error_data = null;
|
||||
this.data.loading = true;
|
||||
}
|
||||
|
||||
async on_load() {
|
||||
try {
|
||||
this.data.project = await Frontend_Projects_Controller
|
||||
.get_project({ id: this.args.id });
|
||||
} catch (e) {
|
||||
this.data.error_data = e;
|
||||
}
|
||||
this.data.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
The template uses the three-state pattern with full page content
|
||||
in the success state. See the actual file for complete template.
|
||||
|
||||
WHEN TO USE THIS PATTERN
|
||||
Use for:
|
||||
- Detail/view pages loading a single record by ID
|
||||
- Edit pages that need to load existing data
|
||||
- Any page where initial data might not exist or be accessible
|
||||
|
||||
Not needed for:
|
||||
- List pages (DataGrid handles its own loading/error states)
|
||||
- Create pages (no existing data to load)
|
||||
- Static pages without dynamic data
|
||||
|
||||
SEE ALSO
|
||||
spa(3), ajax_error_handling(3), jqhtml(3)
|
||||
|
||||
RSX Framework 2025-11-21 VIEW_ACTION_PATTERNS(3)
|
||||
@@ -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
|
||||
|
||||
```
|
||||
|
||||
@@ -26,6 +26,11 @@ This separation ensures:
|
||||
|
||||
**PURPOSE**: Essential directives for AI/LLM assistants developing RSX applications with RSpade.
|
||||
|
||||
## CRITICAL: Questions vs Commands
|
||||
|
||||
- **Questions get answers, NOT actions** - "Is that fire?" gets "Yes" not "Let me run through it". User has a plan, don't take destructive action when asked a question.
|
||||
- **Commands get implementation** - Clear directives result in code changes
|
||||
|
||||
## What is RSpade?
|
||||
|
||||
**Visual Basic-like development for PHP/Laravel.** Think: VB6 apps → VB6 runtime → Windows = RSX apps → RSpade runtime → Laravel.
|
||||
@@ -314,7 +319,7 @@ class Frontend_Layout extends Spa_Layout {
|
||||
}
|
||||
```
|
||||
|
||||
Layout template must have `$id="content"` element where actions render.
|
||||
Layout template must have `$sid="content"` element where actions render.
|
||||
|
||||
### URL Generation & Navigation
|
||||
|
||||
@@ -363,6 +368,30 @@ class Contacts_View_Action extends Spa_Action {
|
||||
|
||||
Details: `php artisan rsx:man spa`
|
||||
|
||||
### View Action Pattern (Loading Data)
|
||||
|
||||
For SPA actions that load data (view/edit CRUD pages), use the three-state pattern:
|
||||
|
||||
```javascript
|
||||
on_create() {
|
||||
this.data.record = { name: '' }; // Stub prevents undefined errors
|
||||
this.data.error_data = null;
|
||||
this.data.loading = true;
|
||||
}
|
||||
async on_load() {
|
||||
try {
|
||||
this.data.record = await Controller.get({id: this.args.id});
|
||||
} catch (e) {
|
||||
this.data.error_data = e;
|
||||
}
|
||||
this.data.loading = false;
|
||||
}
|
||||
```
|
||||
|
||||
Template uses three states: `<Loading_Spinner>` → `<Universal_Error_Page_Component>` → content.
|
||||
|
||||
**Details**: `php artisan rsx:man view_action_patterns`
|
||||
|
||||
---
|
||||
|
||||
## CONVERTING BLADE PAGES TO SPA ACTIONS
|
||||
@@ -415,7 +444,7 @@ class Feature_Index_Action extends Spa_Action {
|
||||
```php
|
||||
// Remove #[Route] method completely. Add Ajax endpoints:
|
||||
#[Ajax_Endpoint]
|
||||
public static function fetch_items(Request $request, array $params = []): array {
|
||||
public static function fetch_items(Request $request, array $params = []) {
|
||||
return ['items' => Feature_Model::all()];
|
||||
}
|
||||
```
|
||||
@@ -615,6 +644,10 @@ For mechanical thinkers who see structure, not visuals. Write `<User_Card>` not
|
||||
directly in attribute context. Works with static values, interpolations, and multiple conditions per element.
|
||||
Example: `<input <% if (this.args.required) { %>required="required"<% } %> />`
|
||||
|
||||
**Inline Logic**: `<% this.handler = () => action(); %>` then `@click=this.handler` - No JS file needed for simple components
|
||||
**Event Handlers**: `@click=this.method` (unquoted) - Methods defined inline or in companion .js
|
||||
**Validation**: `<% if (!this.args.required) throw new Error('Missing arg'); %>` - Fail loud in template
|
||||
|
||||
### 🔴 State Management Rules (ENFORCED)
|
||||
|
||||
**this.args** - Component arguments (read-only in on_load(), modifiable elsewhere)
|
||||
@@ -702,21 +735,21 @@ async on_load() {
|
||||
|
||||
- **`$quoted="string"`** → String literal
|
||||
- **`$unquoted=expression`** → JavaScript expression
|
||||
- **`$id="name"`** → Scoped element ID
|
||||
- **`$sid="name"`** → Scoped element ID
|
||||
|
||||
### Component Access
|
||||
|
||||
**this.$id(name)** → jQuery object (for DOM):
|
||||
**this.$sid(name)** → jQuery object (for DOM):
|
||||
```javascript
|
||||
this.$id('button').on('click', ...);
|
||||
this.$sid('button').on('click', ...);
|
||||
```
|
||||
|
||||
**this.id(name)** → Component instance (for methods):
|
||||
**this.sid(name)** → Component instance (for methods):
|
||||
```javascript
|
||||
const comp = this.id('child'); // ✅ Returns component
|
||||
const comp = this.sid('child'); // ✅ Returns component
|
||||
await comp.reload();
|
||||
|
||||
const comp = this.id('child').component(); // ❌ WRONG
|
||||
const comp = this.sid('child').component(); // ❌ WRONG
|
||||
```
|
||||
|
||||
### Incremental Scaffolding
|
||||
@@ -741,7 +774,7 @@ const comp = this.id('child').component(); // ❌ WRONG
|
||||
7. Use `Controller.method()` not `$.ajax()`
|
||||
8. Blade components self-closing only
|
||||
9. `on_create/render/stop` must be sync
|
||||
10. Use this.id() for components, NOT this.id().component()
|
||||
10. Use this.sid() for components, NOT this.sid().component()
|
||||
|
||||
### Bundle Integration Required
|
||||
|
||||
@@ -786,11 +819,11 @@ class My_Form extends Component {
|
||||
vals(values) {
|
||||
if (values) {
|
||||
// Setter - populate form
|
||||
this.$id('name').val(values.name || '');
|
||||
this.$sid('name').val(values.name || '');
|
||||
return null;
|
||||
} else {
|
||||
// Getter - extract values
|
||||
return {name: this.$id('name').val()};
|
||||
return {name: this.$sid('name').val()};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -829,7 +862,7 @@ const result = await Modal.form({
|
||||
});
|
||||
```
|
||||
|
||||
**Requirements**: Form component must implement `vals()` and include `<div $id="error_container"></div>`.
|
||||
**Requirements**: Form component must implement `vals()` and include `<div $sid="error_container"></div>`.
|
||||
|
||||
**Modal Classes** (for complex/reusable modals):
|
||||
```javascript
|
||||
@@ -963,47 +996,49 @@ Details: `php artisan rsx:man file_upload`
|
||||
|
||||
## AJAX ENDPOINTS
|
||||
|
||||
**Return data directly - framework auto-wraps as `{success: true, data: ...}`**
|
||||
|
||||
```php
|
||||
#[Ajax_Endpoint]
|
||||
public static function fetch_client(Request $request, array $params = []) {
|
||||
$client = Client_Model::find($params['id']);
|
||||
if (!$client) throw new \Exception('Not found');
|
||||
return $client; // ✅ Return data directly
|
||||
public static function method(Request $request, array $params = []) {
|
||||
return $data; // Success - framework wraps as {_success: true, _ajax_return_value: ...}
|
||||
}
|
||||
```
|
||||
|
||||
**❌ NEVER**: `return ['success' => true, 'data' => $client]` - framework adds this
|
||||
**Call:** `await Controller.method({param: value})`
|
||||
|
||||
**Special returns** (bypass auto-wrap):
|
||||
- `['errors' => [...]]` - Form validation errors
|
||||
- `['redirect' => '/path']` - Client-side navigation
|
||||
- `['error' => 'msg']` - Operation failure
|
||||
### Error Responses
|
||||
|
||||
Use `response_error(Ajax::ERROR_CODE, $metadata)`:
|
||||
|
||||
```php
|
||||
// Form validation
|
||||
if (empty($params['name'])) {
|
||||
return ['errors' => ['name' => 'Required']];
|
||||
// Not found
|
||||
return response_error(Ajax::ERROR_NOT_FOUND, 'Project not found');
|
||||
|
||||
// Validation
|
||||
return response_error(Ajax::ERROR_VALIDATION, [
|
||||
'email' => 'Invalid',
|
||||
'name' => 'Required'
|
||||
]);
|
||||
|
||||
// Auto-message
|
||||
return response_error(Ajax::ERROR_UNAUTHORIZED);
|
||||
```
|
||||
|
||||
**Codes:** `ERROR_VALIDATION`, `ERROR_NOT_FOUND`, `ERROR_UNAUTHORIZED`, `ERROR_AUTH_REQUIRED`, `ERROR_FATAL`, `ERROR_GENERIC`
|
||||
|
||||
**Client:**
|
||||
```javascript
|
||||
try {
|
||||
const data = await Controller.get(id);
|
||||
} catch (e) {
|
||||
if (e.code === Ajax.ERROR_NOT_FOUND) {
|
||||
// Handle
|
||||
} else {
|
||||
alert(e.message); // Generic
|
||||
}
|
||||
|
||||
// Success with redirect
|
||||
Flash_Alert::success('Saved');
|
||||
return ['redirect' => Rsx::Route('View_Action', $id)];
|
||||
|
||||
// Permission error
|
||||
if (!can_delete()) {
|
||||
return ['error' => 'Permission denied'];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Flash Alerts in Ajax Endpoints
|
||||
|
||||
Use server-side success alerts ONLY with redirects (no on-screen element to show message).
|
||||
|
||||
**Client-side**: Use `Flash_Alert.error()`, `Flash_Alert.warning()` on case-by-case basis.
|
||||
Unhandled errors auto-show flash alert.
|
||||
|
||||
---
|
||||
|
||||
|
||||
50
node_modules/.package-lock.json
generated
vendored
50
node_modules/.package-lock.json
generated
vendored
@@ -2211,9 +2211,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jqhtml/core": {
|
||||
"version": "2.2.217",
|
||||
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/core/-/core-2.2.217.tgz",
|
||||
"integrity": "sha512-rmu7jgRM3PPvKGkFbRZ0wTXBxHPuvVf9aIMSXv6n0KceTvKlqLR2EFHjPrHbCp9X0DyzoooiVhKEJcKlbVIXJw==",
|
||||
"version": "2.2.218",
|
||||
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/core/-/core-2.2.218.tgz",
|
||||
"integrity": "sha512-CEbrpoi70Y5ET1fBXHK38fZTi5MAtZEY2c779mwjw3Dn7SjpoxvCYet/AJLJI7lngA6eJl9BPaPwALMHbafkLQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
@@ -2237,9 +2237,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jqhtml/parser": {
|
||||
"version": "2.2.217",
|
||||
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/parser/-/parser-2.2.217.tgz",
|
||||
"integrity": "sha512-pAQegkFNZSY+DjkrvQ162c78lg9YU2gCaftYxWdLTrKP86mr0QVG8vAQ8IsvH/sQBvVRUIR9sNSjK+fXd/SnlQ==",
|
||||
"version": "2.2.218",
|
||||
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/parser/-/parser-2.2.218.tgz",
|
||||
"integrity": "sha512-i8Y/tx/mIwhAw8YZd99j2i8iLWSU/9Ua88u4p0h3sCCeIWGT7QGvrr7vF+OiUcb8ECtX1188ODlHWdWt7vb45w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/jest": "^29.5.11",
|
||||
@@ -2257,9 +2257,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jqhtml/router": {
|
||||
"version": "2.2.217",
|
||||
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/router/-/router-2.2.217.tgz",
|
||||
"integrity": "sha512-WiUAOb92sDY3kRD8lmsxcd8oSNqVmCDMsxZj0z/KRLCa0Y6kGrtk8AlQiunBQzvEHGVIb/Kt/6P1WhKEKaBh/g==",
|
||||
"version": "2.2.218",
|
||||
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/router/-/router-2.2.218.tgz",
|
||||
"integrity": "sha512-aQum/FdDlqbtNbtkIJFN5sGNTBhlGBn5duclsyv0CYmJ8ruC2Gr0y5FILBeuc1lFSmG/6UJZ+eOlrQ4QDk2zng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
@@ -2277,21 +2277,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jqhtml/vscode-extension": {
|
||||
"version": "2.2.217",
|
||||
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/vscode-extension/-/vscode-extension-2.2.217.tgz",
|
||||
"integrity": "sha512-6AXZnG03DFi78tdCv21dQF5ILt/YCVPGqZpFKIBNH+CaNccCFW415fMvfC6jNhm34fUSMu0MCHmWSGXzamCtGQ==",
|
||||
"version": "2.2.218",
|
||||
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/vscode-extension/-/vscode-extension-2.2.218.tgz",
|
||||
"integrity": "sha512-fEOcYqi2AVkLxxJ3ovwKqJQGpLI9C6sgcRWs3HVuDH6UYpNiRPUwzSxf/M7j+wZY5y5tt7KGSDK5JjbSj0PqqQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"vscode": "^1.74.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jqhtml/webpack-loader": {
|
||||
"version": "2.2.217",
|
||||
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/webpack-loader/-/webpack-loader-2.2.217.tgz",
|
||||
"integrity": "sha512-zUquYIBEEodMp5xypVbR7dbrsaII6Ojux5i93HS68sMfWzafXr67mEGyjv9ls0sZ47SD8L37iQef5sh3OVRxww==",
|
||||
"version": "2.2.218",
|
||||
"resolved": "http://privatenpm.hanson.xyz/@jqhtml/webpack-loader/-/webpack-loader-2.2.218.tgz",
|
||||
"integrity": "sha512-QN/4qTsxPjB9OWHdNVvif05ygw9FYblF6KYyHPvdZ0NNWNoUMYBcKizjjTjFAsUMqWQkT4RETx5XxkiomgKPPQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jqhtml/parser": "2.2.217",
|
||||
"@jqhtml/parser": "2.2.218",
|
||||
"@types/loader-utils": "^2.0.6",
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/webpack": "^5.28.5",
|
||||
@@ -4017,9 +4017,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.8.29",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.29.tgz",
|
||||
"integrity": "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA==",
|
||||
"version": "2.8.30",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.30.tgz",
|
||||
"integrity": "sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"baseline-browser-mapping": "dist/cli.js"
|
||||
@@ -5697,9 +5697,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.257",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.257.tgz",
|
||||
"integrity": "sha512-VNSOB6JZan5IQNMqaurYpZC4bDPXcvKlUwVD/ztMeVD7SwOpMYGOY7dgt+4lNiIHIpvv/FdULnZKqKEy2KcuHQ==",
|
||||
"version": "1.5.259",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.259.tgz",
|
||||
"integrity": "sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/elliptic": {
|
||||
@@ -10841,9 +10841,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.94.1",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.94.1.tgz",
|
||||
"integrity": "sha512-/YVm5FRQaRlr3oNh2LLFYne1PdPlRZGyKnHh1sLleOqLcohTR4eUUvBjBIqkl1fEXd1MGOHgzJGJh+LgTtV4KQ==",
|
||||
"version": "1.94.2",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.94.2.tgz",
|
||||
"integrity": "sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
|
||||
8
node_modules/@jqhtml/core/LLM_REFERENCE.md
generated
vendored
8
node_modules/@jqhtml/core/LLM_REFERENCE.md
generated
vendored
@@ -27,7 +27,7 @@
|
||||
- Access component methods via `this.methodName()`
|
||||
|
||||
### Attribute System
|
||||
- `$id="name"` → `id="name:_cid"` (component-scoped ID)
|
||||
- `$sid="name"` → `id="name:_cid"` (component-scoped ID)
|
||||
- `$attr="value"` → `data-attr="value"` (data attributes)
|
||||
- `@click="handler"` → jQuery event binding to component method
|
||||
- `@submit.prevent="handler"` → Event with preventDefault
|
||||
@@ -217,7 +217,7 @@ class UserCard extends Jqhtml_Component {
|
||||
- `this._ready_state` - Lifecycle phase (0=created, 1=rendering, 2=creating, 3=loading, 4=ready)
|
||||
|
||||
### DOM Access Methods
|
||||
- `this.$id('name')` - Get element by scoped ID (returns jQuery object)
|
||||
- `this.$sid('name')` - Get element by scoped ID (returns jQuery object)
|
||||
- `this.$child('name')` - Get child component by name
|
||||
- `this.$children()` - Get all direct child components
|
||||
- `this.$parent()` - Get parent component
|
||||
@@ -260,11 +260,11 @@ class CustomInput extends Component {
|
||||
val(value) {
|
||||
if (arguments.length === 0) {
|
||||
// Getter - return processed value
|
||||
return this.parse_value(this.$id('input').val());
|
||||
return this.parse_value(this.$sid('input').val());
|
||||
} else {
|
||||
// Setter - validate and set
|
||||
if (this.validate(value)) {
|
||||
this.$id('input').val(this.format_value(value));
|
||||
this.$sid('input').val(this.format_value(value));
|
||||
this.data.value = value;
|
||||
}
|
||||
return this.$; // Maintain jQuery chaining
|
||||
|
||||
20
node_modules/@jqhtml/core/README.md
generated
vendored
20
node_modules/@jqhtml/core/README.md
generated
vendored
@@ -132,7 +132,7 @@ If you need to show different DOM states before and after loading, use these pat
|
||||
class DataComponent extends Component {
|
||||
async on_create() {
|
||||
// Set loading state in the DOM during create phase
|
||||
this.$id('status').addClass('loading').text('Loading...');
|
||||
this.$sid('status').addClass('loading').text('Loading...');
|
||||
}
|
||||
|
||||
async on_load() {
|
||||
@@ -142,8 +142,8 @@ class DataComponent extends Component {
|
||||
|
||||
async on_ready() {
|
||||
// Update DOM after data is loaded
|
||||
this.$id('status').removeClass('loading').text('Loaded');
|
||||
this.$id('username').text(this.data.user.name);
|
||||
this.$sid('status').removeClass('loading').text('Loaded');
|
||||
this.$sid('username').text(this.data.user.name);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -153,12 +153,12 @@ class DataComponent extends Component {
|
||||
class BadComponent extends Component {
|
||||
async on_load() {
|
||||
// ❌ WRONG - DOM modification in load()
|
||||
this.$id('status').text('Loading...'); // VIOLATION!
|
||||
this.$sid('status').text('Loading...'); // VIOLATION!
|
||||
|
||||
this.data.user = await fetch('/api/user').then(r => r.json());
|
||||
|
||||
// ❌ WRONG - More DOM modification
|
||||
this.$id('status').text('Loaded'); // VIOLATION!
|
||||
this.$sid('status').text('Loaded'); // VIOLATION!
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -261,9 +261,9 @@ class TabsComponent extends Component {
|
||||
|
||||
selectTab(tabId) {
|
||||
// Use $id() for scoped selection
|
||||
this.$id('tab1').removeClass('active');
|
||||
this.$id('tab2').removeClass('active');
|
||||
this.$id(tabId).addClass('active');
|
||||
this.$sid('tab1').removeClass('active');
|
||||
this.$sid('tab2').removeClass('active');
|
||||
this.$sid(tabId).addClass('active');
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -300,7 +300,7 @@ JQHTML has specific rules for attribute quoting and value passing:
|
||||
- **@ Event attributes**: MUST be unquoted (pass function references)
|
||||
Example: `@click=this.handleClick`
|
||||
- **$ Data attributes**: Can be quoted OR unquoted (flexible)
|
||||
Example: `$id="my-id"` or `$data=this.complexObject`
|
||||
Example: `$sid="my-id"` or `$data=this.complexObject`
|
||||
- **Regular HTML attributes**: MUST be quoted (strings only)
|
||||
Example: `class="container <%= this.args.theme %>"`
|
||||
|
||||
@@ -375,7 +375,7 @@ $('#user-profile').component().on('ready', (component) => {
|
||||
console.log('User data:', component.data.user);
|
||||
|
||||
// Safe to access all DOM elements
|
||||
component.$id('email').addClass('verified');
|
||||
component.$sid('email').addClass('verified');
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
0
node_modules/@jqhtml/core/dist/component-registry.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/component-registry.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/component-registry.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/component-registry.d.ts.map
generated
vendored
Executable file → Normal file
28
node_modules/@jqhtml/core/dist/component.d.ts
generated
vendored
Executable file → Normal file
28
node_modules/@jqhtml/core/dist/component.d.ts
generated
vendored
Executable file → Normal file
@@ -193,6 +193,17 @@ export declare class Jqhtml_Component {
|
||||
on_load(): Promise<void>;
|
||||
on_ready(): Promise<void>;
|
||||
on_stop(): void | Promise<void>;
|
||||
/**
|
||||
* Optional: Override cache key generation
|
||||
*
|
||||
* By default, cache keys are generated from component name + args.
|
||||
* Override this method to provide a custom cache key for this component instance.
|
||||
*
|
||||
* If this method throws an error, caching will be disabled for this component.
|
||||
*
|
||||
* @returns Custom cache key string (will be prefixed with component name)
|
||||
*/
|
||||
cache_id?(): string;
|
||||
/**
|
||||
* Should component re-render after load?
|
||||
* By default, only re-renders if data has changed
|
||||
@@ -231,9 +242,9 @@ export declare class Jqhtml_Component {
|
||||
* Searches for elements with id="local_id:THIS_COMPONENT_CID"
|
||||
*
|
||||
* Example:
|
||||
* Template: <button $id="save_btn">Save</button>
|
||||
* Rendered: <button id="save_btn:abc123" data-id="save_btn">Save</button>
|
||||
* Access: this.$id('save_btn') // Returns jQuery element
|
||||
* Template: <button $sid="save_btn">Save</button>
|
||||
* Rendered: <button id="save_btn:abc123" data-sid="save_btn">Save</button>
|
||||
* Access: this.$sid('save_btn') // Returns jQuery element
|
||||
*
|
||||
* Performance: Uses native document.getElementById() when component is in DOM,
|
||||
* falls back to jQuery.find() for components not yet attached to DOM.
|
||||
@@ -241,21 +252,24 @@ export declare class Jqhtml_Component {
|
||||
* @param local_id The local ID (without _cid suffix)
|
||||
* @returns jQuery element with id="local_id:_cid", or empty jQuery object if not found
|
||||
*/
|
||||
$id(local_id: string): any;
|
||||
$sid(local_id: string): any;
|
||||
/**
|
||||
* Get component instance by scoped ID
|
||||
*
|
||||
* Convenience method that finds element by scoped ID and returns the component instance.
|
||||
*
|
||||
* Example:
|
||||
* Template: <User_Card $id="active_user" />
|
||||
* Access: const user = this.id('active_user'); // Returns User_Card instance
|
||||
* Template: <User_Card $sid="active_user" />
|
||||
* Access: const user = this.sid('active_user'); // Returns User_Card instance
|
||||
* user.data.name // Access component's data
|
||||
*
|
||||
* To get the scoped ID string itself:
|
||||
* this.$sid('active_user').attr('id') // Returns "active_user:abc123xyz"
|
||||
*
|
||||
* @param local_id The local ID (without _cid suffix)
|
||||
* @returns Component instance or null if not found or not a component
|
||||
*/
|
||||
id(local_id: string): Jqhtml_Component | null;
|
||||
sid(local_id: string): Jqhtml_Component | null;
|
||||
/**
|
||||
* Get the component that instantiated this component (rendered it in their template)
|
||||
* Returns null if component was created programmatically via $().component()
|
||||
|
||||
2
node_modules/@jqhtml/core/dist/component.d.ts.map
generated
vendored
Executable file → Normal file
2
node_modules/@jqhtml/core/dist/component.d.ts.map
generated
vendored
Executable file → Normal 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
0
node_modules/@jqhtml/core/dist/debug-entry.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/debug-entry.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/debug-entry.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/debug-overlay.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/debug-overlay.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/debug-overlay.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/debug-overlay.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/debug.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/debug.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/debug.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/debug.d.ts.map
generated
vendored
Executable file → Normal file
130
node_modules/@jqhtml/core/dist/index.cjs
generated
vendored
Executable file → Normal file
130
node_modules/@jqhtml/core/dist/index.cjs
generated
vendored
Executable file → Normal file
@@ -499,7 +499,7 @@ function process_tag_to_html(instruction, html, tagElements, components, context
|
||||
if (key === 'id' && tid) {
|
||||
// Special handling for id attribute - scope to parent component's _cid
|
||||
// This is for regular id="foo" attributes that need scoping (rare case)
|
||||
// Most scoping happens via $id attribute which becomes data-id
|
||||
// Most scoping happens via $sid attribute which becomes data-sid
|
||||
// Don't double-scope if already scoped (contains :)
|
||||
if (typeof value === 'string' && value.includes(':')) {
|
||||
html.push(` id="${value}"`);
|
||||
@@ -549,13 +549,13 @@ function process_component_to_html(instruction, html, components, context) {
|
||||
// Create element with tracking ID
|
||||
html.push(`<${tagName} data-cid="${cid}"`);
|
||||
// Handle id attributes for components
|
||||
// The compiled code always generates both 'id' (scoped) and 'data-id' (base) for $id attributes
|
||||
// The compiled code always generates both 'id' (scoped) and 'data-sid' (base) for $sid attributes
|
||||
// We just pass through what the compiler gave us - NEVER regenerate
|
||||
if (props['data-id']) {
|
||||
const baseId = props['data-id'];
|
||||
if (props['data-sid']) {
|
||||
const baseId = props['data-sid'];
|
||||
// The compiled code ALWAYS sets props['id'] with the correct scoped value
|
||||
// Just use it directly - it already has the correct parent _cid baked in
|
||||
html.push(` id="${props['id']}" data-id="${baseId}"`);
|
||||
html.push(` id="${props['id']}" data-sid="${baseId}"`);
|
||||
}
|
||||
// Regular id passes through unchanged
|
||||
else if (props['id']) {
|
||||
@@ -1229,16 +1229,16 @@ class Jqhtml_Component {
|
||||
// If id provided, delegate to child component
|
||||
if (id) {
|
||||
// First check if element with scoped ID exists
|
||||
const $element = this.$id(id);
|
||||
const $element = this.$sid(id);
|
||||
if ($element.length === 0) {
|
||||
throw new Error(`[JQHTML] render("${id}") - no such id.\n` +
|
||||
`Component "${this.component_name()}" has no child element with $id="${id}".`);
|
||||
`Component "${this.component_name()}" has no child element with $sid="${id}".`);
|
||||
}
|
||||
// Element exists, check if it's a component
|
||||
const child = $element.data('_component');
|
||||
if (!child) {
|
||||
throw new Error(`[JQHTML] render("${id}") - element is not a component or does not have $redrawable attribute set.\n` +
|
||||
`Element with $id="${id}" exists but is not initialized as a component.\n` +
|
||||
`Element with $sid="${id}" exists but is not initialized as a component.\n` +
|
||||
`Add $redrawable attribute or make it a proper component.`);
|
||||
}
|
||||
return child._render();
|
||||
@@ -1459,15 +1459,15 @@ class Jqhtml_Component {
|
||||
return;
|
||||
// If id provided, delegate to child component
|
||||
if (id) {
|
||||
const $element = this.$id(id);
|
||||
const $element = this.$sid(id);
|
||||
if ($element.length === 0) {
|
||||
throw new Error(`[JQHTML] render("${id}") - no such id.\n` +
|
||||
`Component "${this.component_name()}" has no child element with $id="${id}".`);
|
||||
`Component "${this.component_name()}" has no child element with $sid="${id}".`);
|
||||
}
|
||||
const child = $element.data('_component');
|
||||
if (!child) {
|
||||
throw new Error(`[JQHTML] render("${id}") - element is not a component or does not have $redrawable attribute set.\n` +
|
||||
`Element with $id="${id}" exists but is not initialized as a component.\n` +
|
||||
`Element with $sid="${id}" exists but is not initialized as a component.\n` +
|
||||
`Add $redrawable attribute or make it a proper component.`);
|
||||
}
|
||||
return child.render();
|
||||
@@ -1515,8 +1515,26 @@ class Jqhtml_Component {
|
||||
// This happens after on_create() but before render, allowing instant first render with cached data
|
||||
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
|
||||
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
|
||||
const { key: cache_key, uncacheable_property } = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
||||
// If cache_key is null, args are not serializable - skip caching
|
||||
// Check if component implements cache_id() for custom cache key
|
||||
let cache_key = null;
|
||||
let uncacheable_property;
|
||||
if (typeof this.cache_id === 'function') {
|
||||
try {
|
||||
const custom_cache_id = this.cache_id();
|
||||
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
|
||||
}
|
||||
catch (error) {
|
||||
// cache_id() threw error - disable caching
|
||||
uncacheable_property = 'cache_id()';
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Use standard args-based cache key generation
|
||||
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
||||
cache_key = result.key;
|
||||
uncacheable_property = result.uncacheable_property;
|
||||
}
|
||||
// If cache_key is null, caching disabled
|
||||
if (cache_key === null) {
|
||||
// Set data-nocache attribute for debugging (shows which property prevented caching)
|
||||
if (uncacheable_property) {
|
||||
@@ -1574,8 +1592,25 @@ class Jqhtml_Component {
|
||||
// Import coordinator and storage lazily to avoid circular dependency
|
||||
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
|
||||
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
|
||||
// Generate cache key (same as deduplication key)
|
||||
const { key: cache_key, uncacheable_property } = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
||||
// Check if component implements cache_id() for custom cache key
|
||||
let cache_key = null;
|
||||
let uncacheable_property;
|
||||
if (typeof this.cache_id === 'function') {
|
||||
try {
|
||||
const custom_cache_id = this.cache_id();
|
||||
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
|
||||
}
|
||||
catch (error) {
|
||||
// cache_id() threw error - disable caching
|
||||
uncacheable_property = 'cache_id()';
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Use standard args-based cache key generation
|
||||
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
||||
cache_key = result.key;
|
||||
uncacheable_property = result.uncacheable_property;
|
||||
}
|
||||
// If cache_key is null, args are not serializable - skip load deduplication and caching
|
||||
if (cache_key === null) {
|
||||
// Set data-nocache attribute for debugging (shows which property prevented caching)
|
||||
@@ -1916,7 +1951,23 @@ class Jqhtml_Component {
|
||||
if (args_changed) {
|
||||
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
|
||||
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
|
||||
const { key: cache_key, uncacheable_property } = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
||||
// Check if component implements cache_id() for custom cache key
|
||||
let cache_key = null;
|
||||
if (typeof this.cache_id === 'function') {
|
||||
try {
|
||||
const custom_cache_id = this.cache_id();
|
||||
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
|
||||
}
|
||||
catch (error) {
|
||||
// cache_id() threw error - disable caching
|
||||
cache_key = null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Use standard args-based cache key generation
|
||||
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
||||
cache_key = result.key;
|
||||
}
|
||||
// Only use cache if args are serializable
|
||||
if (cache_key !== null) {
|
||||
const cached_data = Jqhtml_Local_Storage.get(cache_key);
|
||||
@@ -1955,7 +2006,23 @@ class Jqhtml_Component {
|
||||
if (data_changed && data_after_load !== '{}') {
|
||||
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
|
||||
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
|
||||
const { key: cache_key, uncacheable_property } = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
||||
// Check if component implements cache_id() for custom cache key
|
||||
let cache_key = null;
|
||||
if (typeof this.cache_id === 'function') {
|
||||
try {
|
||||
const custom_cache_id = this.cache_id();
|
||||
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
|
||||
}
|
||||
catch (error) {
|
||||
// cache_id() threw error - disable caching
|
||||
cache_key = null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Use standard args-based cache key generation
|
||||
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
||||
cache_key = result.key;
|
||||
}
|
||||
// Only update cache if args are serializable
|
||||
if (cache_key !== null) {
|
||||
Jqhtml_Local_Storage.set(cache_key, this.data);
|
||||
@@ -2166,9 +2233,9 @@ class Jqhtml_Component {
|
||||
* Searches for elements with id="local_id:THIS_COMPONENT_CID"
|
||||
*
|
||||
* Example:
|
||||
* Template: <button $id="save_btn">Save</button>
|
||||
* Rendered: <button id="save_btn:abc123" data-id="save_btn">Save</button>
|
||||
* Access: this.$id('save_btn') // Returns jQuery element
|
||||
* Template: <button $sid="save_btn">Save</button>
|
||||
* Rendered: <button id="save_btn:abc123" data-sid="save_btn">Save</button>
|
||||
* Access: this.$sid('save_btn') // Returns jQuery element
|
||||
*
|
||||
* Performance: Uses native document.getElementById() when component is in DOM,
|
||||
* falls back to jQuery.find() for components not yet attached to DOM.
|
||||
@@ -2176,7 +2243,7 @@ class Jqhtml_Component {
|
||||
* @param local_id The local ID (without _cid suffix)
|
||||
* @returns jQuery element with id="local_id:_cid", or empty jQuery object if not found
|
||||
*/
|
||||
$id(local_id) {
|
||||
$sid(local_id) {
|
||||
const scopedId = `${local_id}:${this._cid}`;
|
||||
// Try getElementById first (fast path - works when component is in DOM)
|
||||
const el = document.getElementById(scopedId);
|
||||
@@ -2184,7 +2251,7 @@ class Jqhtml_Component {
|
||||
return $(el);
|
||||
}
|
||||
// Fallback: component not in DOM yet, search within component subtree
|
||||
// This allows $id() to work on components before they're appended to body
|
||||
// This allows $sid() to work on components before they're appended to body
|
||||
// Must escape the ID because it contains ':' which jQuery treats as a pseudo-selector
|
||||
return this.$.find(`#${$.escapeSelector(scopedId)}`);
|
||||
}
|
||||
@@ -2194,19 +2261,22 @@ class Jqhtml_Component {
|
||||
* Convenience method that finds element by scoped ID and returns the component instance.
|
||||
*
|
||||
* Example:
|
||||
* Template: <User_Card $id="active_user" />
|
||||
* Access: const user = this.id('active_user'); // Returns User_Card instance
|
||||
* Template: <User_Card $sid="active_user" />
|
||||
* Access: const user = this.sid('active_user'); // Returns User_Card instance
|
||||
* user.data.name // Access component's data
|
||||
*
|
||||
* To get the scoped ID string itself:
|
||||
* this.$sid('active_user').attr('id') // Returns "active_user:abc123xyz"
|
||||
*
|
||||
* @param local_id The local ID (without _cid suffix)
|
||||
* @returns Component instance or null if not found or not a component
|
||||
*/
|
||||
id(local_id) {
|
||||
const element = this.$id(local_id);
|
||||
sid(local_id) {
|
||||
const element = this.$sid(local_id);
|
||||
const component = element.data('_component');
|
||||
// If no component found but element exists, warn developer
|
||||
if (!component && element.length > 0) {
|
||||
console.warn(`Component ${this.constructor.name} tried to call .id('${local_id}') - ` +
|
||||
console.warn(`Component ${this.constructor.name} tried to call .sid('${local_id}') - ` +
|
||||
`${local_id} exists, however, it is not a component or $redrawable. ` +
|
||||
`Did you forget to add $redrawable to the tag?`);
|
||||
}
|
||||
@@ -2825,7 +2895,7 @@ function evaluate_expression(expression, component, locals = {}) {
|
||||
args: component.args,
|
||||
$: component.$,
|
||||
// Component methods
|
||||
$id: component.$id.bind(component),
|
||||
$sid: component.$sid.bind(component),
|
||||
// Locals (like $event)
|
||||
...locals
|
||||
};
|
||||
@@ -2853,7 +2923,7 @@ function evaluate_handler(expression, component) {
|
||||
// Otherwise treat as inline code
|
||||
try {
|
||||
return new Function('$event', `
|
||||
const { data, args, $, emit, $id } = this;
|
||||
const { data, args, $, emit, $sid } = this;
|
||||
${expression}
|
||||
`).bind(component);
|
||||
}
|
||||
@@ -4151,7 +4221,7 @@ function init(jQuery) {
|
||||
}
|
||||
}
|
||||
// Version - will be replaced during build with actual version from package.json
|
||||
const version = '2.2.217';
|
||||
const version = '2.2.218';
|
||||
// Default export with all functionality
|
||||
const jqhtml = {
|
||||
// Core
|
||||
|
||||
2
node_modules/@jqhtml/core/dist/index.cjs.map
generated
vendored
Executable file → Normal file
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
0
node_modules/@jqhtml/core/dist/index.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/index.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/index.d.ts.map
generated
vendored
Executable file → Normal file
130
node_modules/@jqhtml/core/dist/index.js
generated
vendored
Executable file → Normal file
130
node_modules/@jqhtml/core/dist/index.js
generated
vendored
Executable file → Normal file
@@ -495,7 +495,7 @@ function process_tag_to_html(instruction, html, tagElements, components, context
|
||||
if (key === 'id' && tid) {
|
||||
// Special handling for id attribute - scope to parent component's _cid
|
||||
// This is for regular id="foo" attributes that need scoping (rare case)
|
||||
// Most scoping happens via $id attribute which becomes data-id
|
||||
// Most scoping happens via $sid attribute which becomes data-sid
|
||||
// Don't double-scope if already scoped (contains :)
|
||||
if (typeof value === 'string' && value.includes(':')) {
|
||||
html.push(` id="${value}"`);
|
||||
@@ -545,13 +545,13 @@ function process_component_to_html(instruction, html, components, context) {
|
||||
// Create element with tracking ID
|
||||
html.push(`<${tagName} data-cid="${cid}"`);
|
||||
// Handle id attributes for components
|
||||
// The compiled code always generates both 'id' (scoped) and 'data-id' (base) for $id attributes
|
||||
// The compiled code always generates both 'id' (scoped) and 'data-sid' (base) for $sid attributes
|
||||
// We just pass through what the compiler gave us - NEVER regenerate
|
||||
if (props['data-id']) {
|
||||
const baseId = props['data-id'];
|
||||
if (props['data-sid']) {
|
||||
const baseId = props['data-sid'];
|
||||
// The compiled code ALWAYS sets props['id'] with the correct scoped value
|
||||
// Just use it directly - it already has the correct parent _cid baked in
|
||||
html.push(` id="${props['id']}" data-id="${baseId}"`);
|
||||
html.push(` id="${props['id']}" data-sid="${baseId}"`);
|
||||
}
|
||||
// Regular id passes through unchanged
|
||||
else if (props['id']) {
|
||||
@@ -1225,16 +1225,16 @@ class Jqhtml_Component {
|
||||
// If id provided, delegate to child component
|
||||
if (id) {
|
||||
// First check if element with scoped ID exists
|
||||
const $element = this.$id(id);
|
||||
const $element = this.$sid(id);
|
||||
if ($element.length === 0) {
|
||||
throw new Error(`[JQHTML] render("${id}") - no such id.\n` +
|
||||
`Component "${this.component_name()}" has no child element with $id="${id}".`);
|
||||
`Component "${this.component_name()}" has no child element with $sid="${id}".`);
|
||||
}
|
||||
// Element exists, check if it's a component
|
||||
const child = $element.data('_component');
|
||||
if (!child) {
|
||||
throw new Error(`[JQHTML] render("${id}") - element is not a component or does not have $redrawable attribute set.\n` +
|
||||
`Element with $id="${id}" exists but is not initialized as a component.\n` +
|
||||
`Element with $sid="${id}" exists but is not initialized as a component.\n` +
|
||||
`Add $redrawable attribute or make it a proper component.`);
|
||||
}
|
||||
return child._render();
|
||||
@@ -1455,15 +1455,15 @@ class Jqhtml_Component {
|
||||
return;
|
||||
// If id provided, delegate to child component
|
||||
if (id) {
|
||||
const $element = this.$id(id);
|
||||
const $element = this.$sid(id);
|
||||
if ($element.length === 0) {
|
||||
throw new Error(`[JQHTML] render("${id}") - no such id.\n` +
|
||||
`Component "${this.component_name()}" has no child element with $id="${id}".`);
|
||||
`Component "${this.component_name()}" has no child element with $sid="${id}".`);
|
||||
}
|
||||
const child = $element.data('_component');
|
||||
if (!child) {
|
||||
throw new Error(`[JQHTML] render("${id}") - element is not a component or does not have $redrawable attribute set.\n` +
|
||||
`Element with $id="${id}" exists but is not initialized as a component.\n` +
|
||||
`Element with $sid="${id}" exists but is not initialized as a component.\n` +
|
||||
`Add $redrawable attribute or make it a proper component.`);
|
||||
}
|
||||
return child.render();
|
||||
@@ -1511,8 +1511,26 @@ class Jqhtml_Component {
|
||||
// This happens after on_create() but before render, allowing instant first render with cached data
|
||||
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
|
||||
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
|
||||
const { key: cache_key, uncacheable_property } = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
||||
// If cache_key is null, args are not serializable - skip caching
|
||||
// Check if component implements cache_id() for custom cache key
|
||||
let cache_key = null;
|
||||
let uncacheable_property;
|
||||
if (typeof this.cache_id === 'function') {
|
||||
try {
|
||||
const custom_cache_id = this.cache_id();
|
||||
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
|
||||
}
|
||||
catch (error) {
|
||||
// cache_id() threw error - disable caching
|
||||
uncacheable_property = 'cache_id()';
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Use standard args-based cache key generation
|
||||
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
||||
cache_key = result.key;
|
||||
uncacheable_property = result.uncacheable_property;
|
||||
}
|
||||
// If cache_key is null, caching disabled
|
||||
if (cache_key === null) {
|
||||
// Set data-nocache attribute for debugging (shows which property prevented caching)
|
||||
if (uncacheable_property) {
|
||||
@@ -1570,8 +1588,25 @@ class Jqhtml_Component {
|
||||
// Import coordinator and storage lazily to avoid circular dependency
|
||||
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
|
||||
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
|
||||
// Generate cache key (same as deduplication key)
|
||||
const { key: cache_key, uncacheable_property } = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
||||
// Check if component implements cache_id() for custom cache key
|
||||
let cache_key = null;
|
||||
let uncacheable_property;
|
||||
if (typeof this.cache_id === 'function') {
|
||||
try {
|
||||
const custom_cache_id = this.cache_id();
|
||||
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
|
||||
}
|
||||
catch (error) {
|
||||
// cache_id() threw error - disable caching
|
||||
uncacheable_property = 'cache_id()';
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Use standard args-based cache key generation
|
||||
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
||||
cache_key = result.key;
|
||||
uncacheable_property = result.uncacheable_property;
|
||||
}
|
||||
// If cache_key is null, args are not serializable - skip load deduplication and caching
|
||||
if (cache_key === null) {
|
||||
// Set data-nocache attribute for debugging (shows which property prevented caching)
|
||||
@@ -1912,7 +1947,23 @@ class Jqhtml_Component {
|
||||
if (args_changed) {
|
||||
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
|
||||
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
|
||||
const { key: cache_key, uncacheable_property } = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
||||
// Check if component implements cache_id() for custom cache key
|
||||
let cache_key = null;
|
||||
if (typeof this.cache_id === 'function') {
|
||||
try {
|
||||
const custom_cache_id = this.cache_id();
|
||||
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
|
||||
}
|
||||
catch (error) {
|
||||
// cache_id() threw error - disable caching
|
||||
cache_key = null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Use standard args-based cache key generation
|
||||
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
||||
cache_key = result.key;
|
||||
}
|
||||
// Only use cache if args are serializable
|
||||
if (cache_key !== null) {
|
||||
const cached_data = Jqhtml_Local_Storage.get(cache_key);
|
||||
@@ -1951,7 +2002,23 @@ class Jqhtml_Component {
|
||||
if (data_changed && data_after_load !== '{}') {
|
||||
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
|
||||
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
|
||||
const { key: cache_key, uncacheable_property } = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
||||
// Check if component implements cache_id() for custom cache key
|
||||
let cache_key = null;
|
||||
if (typeof this.cache_id === 'function') {
|
||||
try {
|
||||
const custom_cache_id = this.cache_id();
|
||||
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
|
||||
}
|
||||
catch (error) {
|
||||
// cache_id() threw error - disable caching
|
||||
cache_key = null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Use standard args-based cache key generation
|
||||
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
||||
cache_key = result.key;
|
||||
}
|
||||
// Only update cache if args are serializable
|
||||
if (cache_key !== null) {
|
||||
Jqhtml_Local_Storage.set(cache_key, this.data);
|
||||
@@ -2162,9 +2229,9 @@ class Jqhtml_Component {
|
||||
* Searches for elements with id="local_id:THIS_COMPONENT_CID"
|
||||
*
|
||||
* Example:
|
||||
* Template: <button $id="save_btn">Save</button>
|
||||
* Rendered: <button id="save_btn:abc123" data-id="save_btn">Save</button>
|
||||
* Access: this.$id('save_btn') // Returns jQuery element
|
||||
* Template: <button $sid="save_btn">Save</button>
|
||||
* Rendered: <button id="save_btn:abc123" data-sid="save_btn">Save</button>
|
||||
* Access: this.$sid('save_btn') // Returns jQuery element
|
||||
*
|
||||
* Performance: Uses native document.getElementById() when component is in DOM,
|
||||
* falls back to jQuery.find() for components not yet attached to DOM.
|
||||
@@ -2172,7 +2239,7 @@ class Jqhtml_Component {
|
||||
* @param local_id The local ID (without _cid suffix)
|
||||
* @returns jQuery element with id="local_id:_cid", or empty jQuery object if not found
|
||||
*/
|
||||
$id(local_id) {
|
||||
$sid(local_id) {
|
||||
const scopedId = `${local_id}:${this._cid}`;
|
||||
// Try getElementById first (fast path - works when component is in DOM)
|
||||
const el = document.getElementById(scopedId);
|
||||
@@ -2180,7 +2247,7 @@ class Jqhtml_Component {
|
||||
return $(el);
|
||||
}
|
||||
// Fallback: component not in DOM yet, search within component subtree
|
||||
// This allows $id() to work on components before they're appended to body
|
||||
// This allows $sid() to work on components before they're appended to body
|
||||
// Must escape the ID because it contains ':' which jQuery treats as a pseudo-selector
|
||||
return this.$.find(`#${$.escapeSelector(scopedId)}`);
|
||||
}
|
||||
@@ -2190,19 +2257,22 @@ class Jqhtml_Component {
|
||||
* Convenience method that finds element by scoped ID and returns the component instance.
|
||||
*
|
||||
* Example:
|
||||
* Template: <User_Card $id="active_user" />
|
||||
* Access: const user = this.id('active_user'); // Returns User_Card instance
|
||||
* Template: <User_Card $sid="active_user" />
|
||||
* Access: const user = this.sid('active_user'); // Returns User_Card instance
|
||||
* user.data.name // Access component's data
|
||||
*
|
||||
* To get the scoped ID string itself:
|
||||
* this.$sid('active_user').attr('id') // Returns "active_user:abc123xyz"
|
||||
*
|
||||
* @param local_id The local ID (without _cid suffix)
|
||||
* @returns Component instance or null if not found or not a component
|
||||
*/
|
||||
id(local_id) {
|
||||
const element = this.$id(local_id);
|
||||
sid(local_id) {
|
||||
const element = this.$sid(local_id);
|
||||
const component = element.data('_component');
|
||||
// If no component found but element exists, warn developer
|
||||
if (!component && element.length > 0) {
|
||||
console.warn(`Component ${this.constructor.name} tried to call .id('${local_id}') - ` +
|
||||
console.warn(`Component ${this.constructor.name} tried to call .sid('${local_id}') - ` +
|
||||
`${local_id} exists, however, it is not a component or $redrawable. ` +
|
||||
`Did you forget to add $redrawable to the tag?`);
|
||||
}
|
||||
@@ -2821,7 +2891,7 @@ function evaluate_expression(expression, component, locals = {}) {
|
||||
args: component.args,
|
||||
$: component.$,
|
||||
// Component methods
|
||||
$id: component.$id.bind(component),
|
||||
$sid: component.$sid.bind(component),
|
||||
// Locals (like $event)
|
||||
...locals
|
||||
};
|
||||
@@ -2849,7 +2919,7 @@ function evaluate_handler(expression, component) {
|
||||
// Otherwise treat as inline code
|
||||
try {
|
||||
return new Function('$event', `
|
||||
const { data, args, $, emit, $id } = this;
|
||||
const { data, args, $, emit, $sid } = this;
|
||||
${expression}
|
||||
`).bind(component);
|
||||
}
|
||||
@@ -4147,7 +4217,7 @@ function init(jQuery) {
|
||||
}
|
||||
}
|
||||
// Version - will be replaced during build with actual version from package.json
|
||||
const version = '2.2.217';
|
||||
const version = '2.2.218';
|
||||
// Default export with all functionality
|
||||
const jqhtml = {
|
||||
// Core
|
||||
|
||||
2
node_modules/@jqhtml/core/dist/index.js.map
generated
vendored
Executable file → Normal file
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
0
node_modules/@jqhtml/core/dist/instruction-processor.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/instruction-processor.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/instruction-processor.d.ts.map
generated
vendored
Executable file → Normal file
132
node_modules/@jqhtml/core/dist/jqhtml-core.esm.js
generated
vendored
Executable file → Normal file
132
node_modules/@jqhtml/core/dist/jqhtml-core.esm.js
generated
vendored
Executable file → Normal file
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* JQHTML Core v2.2.217
|
||||
* JQHTML Core v2.2.218
|
||||
* (c) 2025 JQHTML Team
|
||||
* Released under the MIT License
|
||||
*/
|
||||
@@ -500,7 +500,7 @@ function process_tag_to_html(instruction, html, tagElements, components, context
|
||||
if (key === 'id' && tid) {
|
||||
// Special handling for id attribute - scope to parent component's _cid
|
||||
// This is for regular id="foo" attributes that need scoping (rare case)
|
||||
// Most scoping happens via $id attribute which becomes data-id
|
||||
// Most scoping happens via $sid attribute which becomes data-sid
|
||||
// Don't double-scope if already scoped (contains :)
|
||||
if (typeof value === 'string' && value.includes(':')) {
|
||||
html.push(` id="${value}"`);
|
||||
@@ -550,13 +550,13 @@ function process_component_to_html(instruction, html, components, context) {
|
||||
// Create element with tracking ID
|
||||
html.push(`<${tagName} data-cid="${cid}"`);
|
||||
// Handle id attributes for components
|
||||
// The compiled code always generates both 'id' (scoped) and 'data-id' (base) for $id attributes
|
||||
// The compiled code always generates both 'id' (scoped) and 'data-sid' (base) for $sid attributes
|
||||
// We just pass through what the compiler gave us - NEVER regenerate
|
||||
if (props['data-id']) {
|
||||
const baseId = props['data-id'];
|
||||
if (props['data-sid']) {
|
||||
const baseId = props['data-sid'];
|
||||
// The compiled code ALWAYS sets props['id'] with the correct scoped value
|
||||
// Just use it directly - it already has the correct parent _cid baked in
|
||||
html.push(` id="${props['id']}" data-id="${baseId}"`);
|
||||
html.push(` id="${props['id']}" data-sid="${baseId}"`);
|
||||
}
|
||||
// Regular id passes through unchanged
|
||||
else if (props['id']) {
|
||||
@@ -1230,16 +1230,16 @@ class Jqhtml_Component {
|
||||
// If id provided, delegate to child component
|
||||
if (id) {
|
||||
// First check if element with scoped ID exists
|
||||
const $element = this.$id(id);
|
||||
const $element = this.$sid(id);
|
||||
if ($element.length === 0) {
|
||||
throw new Error(`[JQHTML] render("${id}") - no such id.\n` +
|
||||
`Component "${this.component_name()}" has no child element with $id="${id}".`);
|
||||
`Component "${this.component_name()}" has no child element with $sid="${id}".`);
|
||||
}
|
||||
// Element exists, check if it's a component
|
||||
const child = $element.data('_component');
|
||||
if (!child) {
|
||||
throw new Error(`[JQHTML] render("${id}") - element is not a component or does not have $redrawable attribute set.\n` +
|
||||
`Element with $id="${id}" exists but is not initialized as a component.\n` +
|
||||
`Element with $sid="${id}" exists but is not initialized as a component.\n` +
|
||||
`Add $redrawable attribute or make it a proper component.`);
|
||||
}
|
||||
return child._render();
|
||||
@@ -1460,15 +1460,15 @@ class Jqhtml_Component {
|
||||
return;
|
||||
// If id provided, delegate to child component
|
||||
if (id) {
|
||||
const $element = this.$id(id);
|
||||
const $element = this.$sid(id);
|
||||
if ($element.length === 0) {
|
||||
throw new Error(`[JQHTML] render("${id}") - no such id.\n` +
|
||||
`Component "${this.component_name()}" has no child element with $id="${id}".`);
|
||||
`Component "${this.component_name()}" has no child element with $sid="${id}".`);
|
||||
}
|
||||
const child = $element.data('_component');
|
||||
if (!child) {
|
||||
throw new Error(`[JQHTML] render("${id}") - element is not a component or does not have $redrawable attribute set.\n` +
|
||||
`Element with $id="${id}" exists but is not initialized as a component.\n` +
|
||||
`Element with $sid="${id}" exists but is not initialized as a component.\n` +
|
||||
`Add $redrawable attribute or make it a proper component.`);
|
||||
}
|
||||
return child.render();
|
||||
@@ -1516,8 +1516,26 @@ class Jqhtml_Component {
|
||||
// This happens after on_create() but before render, allowing instant first render with cached data
|
||||
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
|
||||
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
|
||||
const { key: cache_key, uncacheable_property } = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
||||
// If cache_key is null, args are not serializable - skip caching
|
||||
// Check if component implements cache_id() for custom cache key
|
||||
let cache_key = null;
|
||||
let uncacheable_property;
|
||||
if (typeof this.cache_id === 'function') {
|
||||
try {
|
||||
const custom_cache_id = this.cache_id();
|
||||
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
|
||||
}
|
||||
catch (error) {
|
||||
// cache_id() threw error - disable caching
|
||||
uncacheable_property = 'cache_id()';
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Use standard args-based cache key generation
|
||||
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
||||
cache_key = result.key;
|
||||
uncacheable_property = result.uncacheable_property;
|
||||
}
|
||||
// If cache_key is null, caching disabled
|
||||
if (cache_key === null) {
|
||||
// Set data-nocache attribute for debugging (shows which property prevented caching)
|
||||
if (uncacheable_property) {
|
||||
@@ -1575,8 +1593,25 @@ class Jqhtml_Component {
|
||||
// Import coordinator and storage lazily to avoid circular dependency
|
||||
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
|
||||
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
|
||||
// Generate cache key (same as deduplication key)
|
||||
const { key: cache_key, uncacheable_property } = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
||||
// Check if component implements cache_id() for custom cache key
|
||||
let cache_key = null;
|
||||
let uncacheable_property;
|
||||
if (typeof this.cache_id === 'function') {
|
||||
try {
|
||||
const custom_cache_id = this.cache_id();
|
||||
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
|
||||
}
|
||||
catch (error) {
|
||||
// cache_id() threw error - disable caching
|
||||
uncacheable_property = 'cache_id()';
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Use standard args-based cache key generation
|
||||
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
||||
cache_key = result.key;
|
||||
uncacheable_property = result.uncacheable_property;
|
||||
}
|
||||
// If cache_key is null, args are not serializable - skip load deduplication and caching
|
||||
if (cache_key === null) {
|
||||
// Set data-nocache attribute for debugging (shows which property prevented caching)
|
||||
@@ -1917,7 +1952,23 @@ class Jqhtml_Component {
|
||||
if (args_changed) {
|
||||
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
|
||||
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
|
||||
const { key: cache_key, uncacheable_property } = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
||||
// Check if component implements cache_id() for custom cache key
|
||||
let cache_key = null;
|
||||
if (typeof this.cache_id === 'function') {
|
||||
try {
|
||||
const custom_cache_id = this.cache_id();
|
||||
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
|
||||
}
|
||||
catch (error) {
|
||||
// cache_id() threw error - disable caching
|
||||
cache_key = null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Use standard args-based cache key generation
|
||||
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
||||
cache_key = result.key;
|
||||
}
|
||||
// Only use cache if args are serializable
|
||||
if (cache_key !== null) {
|
||||
const cached_data = Jqhtml_Local_Storage.get(cache_key);
|
||||
@@ -1956,7 +2007,23 @@ class Jqhtml_Component {
|
||||
if (data_changed && data_after_load !== '{}') {
|
||||
const { Load_Coordinator } = await Promise.resolve().then(function () { return loadCoordinator; });
|
||||
const { Jqhtml_Local_Storage } = await Promise.resolve().then(function () { return localStorage$1; });
|
||||
const { key: cache_key, uncacheable_property } = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
||||
// Check if component implements cache_id() for custom cache key
|
||||
let cache_key = null;
|
||||
if (typeof this.cache_id === 'function') {
|
||||
try {
|
||||
const custom_cache_id = this.cache_id();
|
||||
cache_key = `${this.component_name()}::${String(custom_cache_id)}`;
|
||||
}
|
||||
catch (error) {
|
||||
// cache_id() threw error - disable caching
|
||||
cache_key = null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Use standard args-based cache key generation
|
||||
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
||||
cache_key = result.key;
|
||||
}
|
||||
// Only update cache if args are serializable
|
||||
if (cache_key !== null) {
|
||||
Jqhtml_Local_Storage.set(cache_key, this.data);
|
||||
@@ -2167,9 +2234,9 @@ class Jqhtml_Component {
|
||||
* Searches for elements with id="local_id:THIS_COMPONENT_CID"
|
||||
*
|
||||
* Example:
|
||||
* Template: <button $id="save_btn">Save</button>
|
||||
* Rendered: <button id="save_btn:abc123" data-id="save_btn">Save</button>
|
||||
* Access: this.$id('save_btn') // Returns jQuery element
|
||||
* Template: <button $sid="save_btn">Save</button>
|
||||
* Rendered: <button id="save_btn:abc123" data-sid="save_btn">Save</button>
|
||||
* Access: this.$sid('save_btn') // Returns jQuery element
|
||||
*
|
||||
* Performance: Uses native document.getElementById() when component is in DOM,
|
||||
* falls back to jQuery.find() for components not yet attached to DOM.
|
||||
@@ -2177,7 +2244,7 @@ class Jqhtml_Component {
|
||||
* @param local_id The local ID (without _cid suffix)
|
||||
* @returns jQuery element with id="local_id:_cid", or empty jQuery object if not found
|
||||
*/
|
||||
$id(local_id) {
|
||||
$sid(local_id) {
|
||||
const scopedId = `${local_id}:${this._cid}`;
|
||||
// Try getElementById first (fast path - works when component is in DOM)
|
||||
const el = document.getElementById(scopedId);
|
||||
@@ -2185,7 +2252,7 @@ class Jqhtml_Component {
|
||||
return $(el);
|
||||
}
|
||||
// Fallback: component not in DOM yet, search within component subtree
|
||||
// This allows $id() to work on components before they're appended to body
|
||||
// This allows $sid() to work on components before they're appended to body
|
||||
// Must escape the ID because it contains ':' which jQuery treats as a pseudo-selector
|
||||
return this.$.find(`#${$.escapeSelector(scopedId)}`);
|
||||
}
|
||||
@@ -2195,19 +2262,22 @@ class Jqhtml_Component {
|
||||
* Convenience method that finds element by scoped ID and returns the component instance.
|
||||
*
|
||||
* Example:
|
||||
* Template: <User_Card $id="active_user" />
|
||||
* Access: const user = this.id('active_user'); // Returns User_Card instance
|
||||
* Template: <User_Card $sid="active_user" />
|
||||
* Access: const user = this.sid('active_user'); // Returns User_Card instance
|
||||
* user.data.name // Access component's data
|
||||
*
|
||||
* To get the scoped ID string itself:
|
||||
* this.$sid('active_user').attr('id') // Returns "active_user:abc123xyz"
|
||||
*
|
||||
* @param local_id The local ID (without _cid suffix)
|
||||
* @returns Component instance or null if not found or not a component
|
||||
*/
|
||||
id(local_id) {
|
||||
const element = this.$id(local_id);
|
||||
sid(local_id) {
|
||||
const element = this.$sid(local_id);
|
||||
const component = element.data('_component');
|
||||
// If no component found but element exists, warn developer
|
||||
if (!component && element.length > 0) {
|
||||
console.warn(`Component ${this.constructor.name} tried to call .id('${local_id}') - ` +
|
||||
console.warn(`Component ${this.constructor.name} tried to call .sid('${local_id}') - ` +
|
||||
`${local_id} exists, however, it is not a component or $redrawable. ` +
|
||||
`Did you forget to add $redrawable to the tag?`);
|
||||
}
|
||||
@@ -2826,7 +2896,7 @@ function evaluate_expression(expression, component, locals = {}) {
|
||||
args: component.args,
|
||||
$: component.$,
|
||||
// Component methods
|
||||
$id: component.$id.bind(component),
|
||||
$sid: component.$sid.bind(component),
|
||||
// Locals (like $event)
|
||||
...locals
|
||||
};
|
||||
@@ -2854,7 +2924,7 @@ function evaluate_handler(expression, component) {
|
||||
// Otherwise treat as inline code
|
||||
try {
|
||||
return new Function('$event', `
|
||||
const { data, args, $, emit, $id } = this;
|
||||
const { data, args, $, emit, $sid } = this;
|
||||
${expression}
|
||||
`).bind(component);
|
||||
}
|
||||
@@ -4152,7 +4222,7 @@ function init(jQuery) {
|
||||
}
|
||||
}
|
||||
// Version - will be replaced during build with actual version from package.json
|
||||
const version = '2.2.217';
|
||||
const version = '2.2.218';
|
||||
// Default export with all functionality
|
||||
const jqhtml = {
|
||||
// Core
|
||||
|
||||
2
node_modules/@jqhtml/core/dist/jqhtml-core.esm.js.map
generated
vendored
Executable file → Normal file
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
0
node_modules/@jqhtml/core/dist/jqhtml-debug.esm.js
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/jqhtml-debug.esm.js.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/jqhtml-debug.esm.js.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/jquery-plugin.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/jquery-plugin.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/jquery-plugin.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/jquery-plugin.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/lifecycle-manager.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/lifecycle-manager.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/lifecycle-manager.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/lifecycle-manager.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/load-coordinator.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/load-coordinator.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/load-coordinator.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/load-coordinator.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/local-storage.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/local-storage.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/local-storage.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/local-storage.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/template-renderer.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/template-renderer.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/template-renderer.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/core/dist/template-renderer.d.ts.map
generated
vendored
Executable file → Normal file
2
node_modules/@jqhtml/core/package.json
generated
vendored
Executable file → Normal file
2
node_modules/@jqhtml/core/package.json
generated
vendored
Executable file → Normal file
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@jqhtml/core",
|
||||
"version": "2.2.217",
|
||||
"version": "2.2.218",
|
||||
"description": "Core runtime library for JQHTML",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
26
node_modules/@jqhtml/parser/LLM_REFERENCE.md
generated
vendored
26
node_modules/@jqhtml/parser/LLM_REFERENCE.md
generated
vendored
@@ -177,15 +177,15 @@ JQHTML uses the `$` prefix as a shorthand for data attributes with special handl
|
||||
2. For debugging visibility, if the value is a string or number, it's also set as a DOM attribute
|
||||
3. Objects and arrays are stored in `.data()` but not visible in DOM
|
||||
|
||||
#### Special Case: `$id="name"` → Scoped IDs
|
||||
#### Special Case: `$sid="name"` → Scoped IDs
|
||||
|
||||
The `$id` attribute has special handling for component-scoped element selection:
|
||||
The `$sid` attribute has special handling for component-scoped element selection:
|
||||
|
||||
```jqhtml
|
||||
<Define:UserCard>
|
||||
<div $id="container">
|
||||
<input $id="username" type="text" />
|
||||
<button $id="submit">Submit</button>
|
||||
<div $sid="container">
|
||||
<input $sid="username" type="text" />
|
||||
<button $sid="submit">Submit</button>
|
||||
</div>
|
||||
</Define:UserCard>
|
||||
```
|
||||
@@ -207,8 +207,8 @@ function render(_cid) {
|
||||
class UserCard extends Component {
|
||||
init() {
|
||||
// Find scoped elements
|
||||
const $username = this.$id('username'); // Returns $('#username:123')
|
||||
const $submit = this.$id('submit'); // Returns $('#submit:123')
|
||||
const $username = this.$sid('username'); // Returns $('#username:123')
|
||||
const $submit = this.$sid('submit'); // Returns $('#submit:123')
|
||||
|
||||
$submit.on('click', () => {
|
||||
const value = $username.val();
|
||||
@@ -224,15 +224,15 @@ Component IDs flow through lexical scope naturally:
|
||||
|
||||
```jqhtml
|
||||
<Define:ParentComponent>
|
||||
<div $id="parent-element"> <!-- Gets ParentComponent's _cid -->
|
||||
<div $sid="parent-element"> <!-- Gets ParentComponent's _cid -->
|
||||
<ChildComponent>
|
||||
<div $id="slot-element" /> <!-- Also gets ParentComponent's _cid -->
|
||||
<div $sid="slot-element" /> <!-- Also gets ParentComponent's _cid -->
|
||||
</ChildComponent>
|
||||
</div>
|
||||
</Define:ParentComponent>
|
||||
|
||||
<Define:ChildComponent>
|
||||
<div $id="child-element"> <!-- Gets ChildComponent's _cid -->
|
||||
<div $sid="child-element"> <!-- Gets ChildComponent's _cid -->
|
||||
<%= content() %> <!-- Slot content preserves parent's _cid -->
|
||||
</div>
|
||||
</Define:ChildComponent>
|
||||
@@ -374,9 +374,9 @@ The `@` prefix binds event handlers:
|
||||
|
||||
<!-- Wrapper that adds behavior to innerHTML -->
|
||||
<Define:Collapsible>
|
||||
<div class="collapsible" $id="wrapper">
|
||||
<div class="collapsible" $sid="wrapper">
|
||||
<button $onclick="toggle">Toggle</button>
|
||||
<div class="content" $id="content">
|
||||
<div class="content" $sid="content">
|
||||
<%= content() %> <!-- Wrapped innerHTML -->
|
||||
</div>
|
||||
</div>
|
||||
@@ -481,7 +481,7 @@ Templates compile to three instruction types:
|
||||
{tag: ["div", {"class": "user-card"}, false]}
|
||||
{tag: ["img", {"src": "/avatar.jpg", "alt": "User"}, true]}
|
||||
{tag: ["button", {"onclick": this.handleClick}, false]}
|
||||
{tag: ["div", {"data-id": "header"}, false]} // $id becomes data-id
|
||||
{tag: ["div", {"data-sid": "header"}, false]} // $id becomes data-id
|
||||
```
|
||||
|
||||
### 2. Component Instruction
|
||||
|
||||
2
node_modules/@jqhtml/parser/README.md
generated
vendored
2
node_modules/@jqhtml/parser/README.md
generated
vendored
@@ -390,7 +390,7 @@ These patterns inform v2 implementation decisions, particularly around maintaini
|
||||
- **Expressions**: `<%= expression %>` for output
|
||||
- **Bindings**: `:text`, `:value`, `:class`, `:style` for data binding
|
||||
- **Events**: `@click`, `@change`, etc. for event handlers
|
||||
- **Scoped IDs**: `$id` attribute for component-scoped IDs
|
||||
- **Scoped IDs**: `$sid` attribute for component-scoped IDs
|
||||
|
||||
### Instruction Format
|
||||
|
||||
|
||||
0
node_modules/@jqhtml/parser/dist/ast.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/ast.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/ast.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/ast.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/ast.js
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/ast.js
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/ast.js.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/ast.js.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/codegen.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/codegen.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/codegen.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/codegen.d.ts.map
generated
vendored
Executable file → Normal file
22
node_modules/@jqhtml/parser/dist/codegen.js
generated
vendored
Executable file → Normal file
22
node_modules/@jqhtml/parser/dist/codegen.js
generated
vendored
Executable file → Normal file
@@ -1042,14 +1042,14 @@ export class CodeGenerator {
|
||||
const entries = Object.entries(attrs).flatMap(([key, value]) => {
|
||||
// Convert 'tag' to '_tag' for component invocations
|
||||
const attrKey = key === 'tag' ? '_tag' : key;
|
||||
// Special handling for data-id attribute (from $id) - create scoped id
|
||||
// NOTE: Parser converts $id="foo" → data-id="foo" so we can distinguish from regular id
|
||||
// This generates: id="foo:PARENT_CID" data-id="foo"
|
||||
// Special handling for data-sid attribute (from $sid) - create scoped id
|
||||
// NOTE: Parser converts $sid="foo" → data-sid="foo" so we can distinguish from regular id
|
||||
// This generates: id="foo:PARENT_CID" data-sid="foo"
|
||||
// The :PARENT_CID scoping happens at runtime in instruction-processor.ts
|
||||
if (key === 'data-id') {
|
||||
if (key === 'data-sid') {
|
||||
const id_entries = [];
|
||||
if (value && typeof value === 'object' && value.interpolated) {
|
||||
// Interpolated $id like $id="user<%= index %>"
|
||||
// Interpolated $sid like $sid="user<%= index %>"
|
||||
const parts = value.parts.map((part) => {
|
||||
if (part.type === 'text') {
|
||||
return this.escape_string(part.value);
|
||||
@@ -1060,19 +1060,19 @@ export class CodeGenerator {
|
||||
});
|
||||
const base_id = parts.join(' + ');
|
||||
id_entries.push(`"id": ${base_id} + ":" + this._cid`);
|
||||
id_entries.push(`"data-id": ${base_id}`);
|
||||
id_entries.push(`"data-sid": ${base_id}`);
|
||||
}
|
||||
else if (value && typeof value === 'object' && value.quoted) {
|
||||
// Quoted $id like $id="static"
|
||||
// Quoted $sid like $sid="static"
|
||||
const base_id = this.escape_string(value.value);
|
||||
id_entries.push(`"id": ${base_id} + ":" + this._cid`);
|
||||
id_entries.push(`"data-id": ${base_id}`);
|
||||
id_entries.push(`"data-sid": ${base_id}`);
|
||||
}
|
||||
else {
|
||||
// Simple $id like $id="username" or expression like $id=someVar
|
||||
// Simple $sid like $sid="username" or expression like $sid=someVar
|
||||
const base_id = this.escape_string(String(value));
|
||||
id_entries.push(`"id": ${base_id} + ":" + this._cid`);
|
||||
id_entries.push(`"data-id": ${base_id}`);
|
||||
id_entries.push(`"data-sid": ${base_id}`);
|
||||
}
|
||||
return id_entries;
|
||||
}
|
||||
@@ -1348,7 +1348,7 @@ export class CodeGenerator {
|
||||
for (const [name, component] of this.components) {
|
||||
code += `// Component: ${name}\n`;
|
||||
code += `jqhtml_components.set('${name}', {\n`;
|
||||
code += ` _jqhtml_version: '2.2.217',\n`; // Version will be replaced during build
|
||||
code += ` _jqhtml_version: '2.2.218',\n`; // Version will be replaced during build
|
||||
code += ` name: '${name}',\n`;
|
||||
code += ` tag: '${component.tagName}',\n`;
|
||||
code += ` defaultAttributes: ${this.serializeAttributeObject(component.defaultAttributes)},\n`;
|
||||
|
||||
2
node_modules/@jqhtml/parser/dist/codegen.js.map
generated
vendored
Executable file → Normal file
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
0
node_modules/@jqhtml/parser/dist/compiler.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/compiler.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/compiler.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/compiler.js
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/compiler.js
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/compiler.js.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/compiler.js.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/errors.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/errors.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/errors.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/errors.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/errors.js
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/errors.js
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/errors.js.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/errors.js.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/index.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/index.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/index.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/index.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/index.js
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/index.js
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/index.js.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/index.js.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/integration.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/integration.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/integration.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/integration.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/integration.js
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/integration.js
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/integration.js.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/integration.js.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/lexer.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/lexer.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/lexer.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/lexer.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/lexer.js
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/lexer.js
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/lexer.js.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/lexer.js.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/parser.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/parser.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/parser.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/parser.d.ts.map
generated
vendored
Executable file → Normal file
14
node_modules/@jqhtml/parser/dist/parser.js
generated
vendored
Executable file → Normal file
14
node_modules/@jqhtml/parser/dist/parser.js
generated
vendored
Executable file → Normal file
@@ -109,9 +109,9 @@ export class Parser {
|
||||
}
|
||||
while (!this.check(TokenType.GT) && !this.is_at_end()) {
|
||||
const attr_name = this.consume(TokenType.ATTR_NAME, 'Expected attribute name');
|
||||
// Validate that $id is not used in Define tags
|
||||
if (attr_name.value === '$id') {
|
||||
throw syntaxError('$id is not allowed in <Define:> tags. Component definitions cannot have scoped IDs.', attr_name.line, attr_name.column, this.source, this.filename);
|
||||
// Validate that $sid is not used in Define tags
|
||||
if (attr_name.value === '$sid') {
|
||||
throw syntaxError('$sid is not allowed in <Define:> tags. Component definitions cannot have scoped IDs.', attr_name.line, attr_name.column, this.source, this.filename);
|
||||
}
|
||||
this.consume(TokenType.EQUALS, 'Expected =');
|
||||
const attr_value = this.parse_attribute_value();
|
||||
@@ -535,10 +535,10 @@ export class Parser {
|
||||
}
|
||||
// Handle special attribute prefixes
|
||||
if (name.startsWith('$')) {
|
||||
// Special case: $id becomes data-id (needed for scoped ID system)
|
||||
// Special case: $sid becomes data-sid (needed for scoped ID system)
|
||||
// All other $ attributes stay as-is (handled by instruction-processor.ts)
|
||||
if (name === '$id') {
|
||||
name = 'data-id';
|
||||
if (name === '$sid') {
|
||||
name = 'data-sid';
|
||||
}
|
||||
// Keep $ prefix for other attributes - they get stored via .data() at runtime
|
||||
// Keep the value object intact to preserve quoted/unquoted distinction
|
||||
@@ -781,7 +781,7 @@ export class Parser {
|
||||
' Or set attributes in on_ready() using jQuery:\n' +
|
||||
' ✅ <tag $id="my_element">\n' +
|
||||
' on_ready() {\n' +
|
||||
' if (this.args.required) this.$id(\'my_element\').attr(\'required\', true);\n' +
|
||||
' if (this.args.required) this.$sid(\'my_element\').attr(\'required\', true);\n' +
|
||||
' }';
|
||||
throw error;
|
||||
}
|
||||
|
||||
2
node_modules/@jqhtml/parser/dist/parser.js.map
generated
vendored
Executable file → Normal file
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
0
node_modules/@jqhtml/parser/dist/runtime.d.ts
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/runtime.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/runtime.d.ts.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/runtime.js
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/runtime.js
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/runtime.js.map
generated
vendored
Executable file → Normal file
0
node_modules/@jqhtml/parser/dist/runtime.js.map
generated
vendored
Executable file → Normal file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user