Add JQHTML-EVENT-01 rule, document custom component events, update jqhtml
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
197
app/RSpade/CodeQuality/Rules/JavaScript/JqhtmlCustomEvent_CodeQualityRule.php
Executable file
197
app/RSpade/CodeQuality/Rules/JavaScript/JqhtmlCustomEvent_CodeQualityRule.php
Executable file
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
|
||||
namespace App\RSpade\CodeQuality\Rules\JavaScript;
|
||||
|
||||
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
|
||||
use App\RSpade\CodeQuality\Support\FileSanitizer;
|
||||
use App\RSpade\Core\Manifest\Manifest;
|
||||
|
||||
/**
|
||||
* Detects use of jQuery .trigger() for custom events in jqhtml components.
|
||||
*
|
||||
* Custom events should use the jqhtml event bus (this.trigger()) instead of
|
||||
* jQuery's trigger() because the jqhtml bus supports firing events that
|
||||
* occurred before handlers were registered - critical for component lifecycle.
|
||||
*/
|
||||
class JqhtmlCustomEvent_CodeQualityRule extends CodeQualityRule_Abstract
|
||||
{
|
||||
/**
|
||||
* Standard jQuery/DOM events that are legitimate to trigger via jQuery
|
||||
*/
|
||||
private const ALLOWED_EVENTS = [
|
||||
// Mouse events
|
||||
'click',
|
||||
'dblclick',
|
||||
'mousedown',
|
||||
'mouseup',
|
||||
'mouseover',
|
||||
'mouseout',
|
||||
'mouseenter',
|
||||
'mouseleave',
|
||||
'mousemove',
|
||||
'contextmenu',
|
||||
// Keyboard events
|
||||
'keydown',
|
||||
'keyup',
|
||||
'keypress',
|
||||
// Form events
|
||||
'submit',
|
||||
'change',
|
||||
'input',
|
||||
'focus',
|
||||
'blur',
|
||||
'focusin',
|
||||
'focusout',
|
||||
'select',
|
||||
'reset',
|
||||
// Touch events
|
||||
'touchstart',
|
||||
'touchend',
|
||||
'touchmove',
|
||||
'touchcancel',
|
||||
// Drag events
|
||||
'drag',
|
||||
'dragstart',
|
||||
'dragend',
|
||||
'dragover',
|
||||
'dragenter',
|
||||
'dragleave',
|
||||
'drop',
|
||||
// Scroll/resize
|
||||
'scroll',
|
||||
'resize',
|
||||
// Load events
|
||||
'load',
|
||||
'unload',
|
||||
'error',
|
||||
// Clipboard events
|
||||
'copy',
|
||||
'cut',
|
||||
'paste',
|
||||
];
|
||||
|
||||
public function get_id(): string
|
||||
{
|
||||
return 'JQHTML-EVENT-01';
|
||||
}
|
||||
|
||||
public function get_name(): string
|
||||
{
|
||||
return 'JQHTML Custom Event Check';
|
||||
}
|
||||
|
||||
public function get_description(): string
|
||||
{
|
||||
return 'Custom events in jqhtml components should use this.trigger() instead of jQuery .trigger()';
|
||||
}
|
||||
|
||||
public function get_file_patterns(): array
|
||||
{
|
||||
return ['*.js'];
|
||||
}
|
||||
|
||||
public function get_default_severity(): string
|
||||
{
|
||||
return 'high';
|
||||
}
|
||||
|
||||
public function is_called_during_manifest_scan(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function check(string $file_path, string $contents, array $metadata = []): void
|
||||
{
|
||||
// Skip vendor and node_modules
|
||||
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip CodeQuality directory
|
||||
if (str_contains($file_path, '/CodeQuality/')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get JavaScript class from manifest metadata
|
||||
$js_classes = [];
|
||||
if (isset($metadata['class']) && isset($metadata['extension']) && $metadata['extension'] === 'js') {
|
||||
$js_classes = [$metadata['class']];
|
||||
}
|
||||
|
||||
// If no classes in metadata, nothing to check
|
||||
if (empty($js_classes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check each class to see if it's a JQHTML component
|
||||
foreach ($js_classes as $class_name) {
|
||||
// Use Manifest to check inheritance
|
||||
if (!Manifest::js_is_subclass_of($class_name, 'Component')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The $contents passed to check() is already sanitized (strings removed).
|
||||
// We need the original file content to extract event names from string literals.
|
||||
$original_contents = file_get_contents($file_path);
|
||||
$original_lines = explode("\n", $original_contents);
|
||||
|
||||
// Sanitized lines are used to skip comments
|
||||
$sanitized_lines = explode("\n", $contents);
|
||||
|
||||
// Look for .trigger() calls on this.$ or that.$
|
||||
foreach ($original_lines as $line_number => $line) {
|
||||
$actual_line_number = $line_number + 1;
|
||||
|
||||
// Use sanitized line to check if this is a comment
|
||||
$sanitized_line = $sanitized_lines[$line_number] ?? '';
|
||||
$trimmed = trim($sanitized_line);
|
||||
|
||||
// Skip comments (check sanitized version which preserves comment markers)
|
||||
if (str_starts_with($trimmed, '//') || str_starts_with($trimmed, '*')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for exception comment on this line or previous line (use original)
|
||||
if (str_contains($line, '@' . $this->get_id() . '-EXCEPTION')) {
|
||||
continue;
|
||||
}
|
||||
if ($line_number > 0 && str_contains($original_lines[$line_number - 1], '@' . $this->get_id() . '-EXCEPTION')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Match patterns like: this.$.trigger( or that.$.trigger(
|
||||
// Also match: this.$sid('x').trigger( or that.$sid('x').trigger(
|
||||
if (preg_match('/(this|that)\.\$(?:sid\s*\([^)]+\))?\s*\.\s*trigger\s*\(\s*[\'"]([^\'"]+)[\'"]/', $line, $matches)) {
|
||||
$event_name = $matches[2];
|
||||
|
||||
// Check if it's an allowed standard event
|
||||
if (in_array(strtolower($event_name), self::ALLOWED_EVENTS, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->add_violation(
|
||||
$file_path,
|
||||
$actual_line_number,
|
||||
"Custom event '{$event_name}' triggered via jQuery .trigger() in Component class '{$class_name}'. " .
|
||||
'Use the jqhtml event bus instead.',
|
||||
trim($line),
|
||||
"Use jqhtml event system for custom events:\n\n" .
|
||||
"FIRING EVENTS:\n" .
|
||||
" Instead of: this.\$.trigger('{$event_name}', data)\n" .
|
||||
" Use: this.trigger('{$event_name}', data)\n\n" .
|
||||
"LISTENING FROM PARENT (using \$sid):\n" .
|
||||
" Instead of: this.\$sid('child').on('{$event_name}', handler)\n" .
|
||||
" Use: this.sid('child').on('{$event_name}', handler)\n\n" .
|
||||
"LISTENING FROM EXTERNAL CODE:\n" .
|
||||
" Instead of: \$(element).on('{$event_name}', handler)\n" .
|
||||
" Use: \$(element).component().on('{$event_name}', handler)\n\n" .
|
||||
"WHY: The jqhtml event bus fires callbacks for events that occurred before the handler " .
|
||||
"was registered. This is critical for component lifecycle - parent components may " .
|
||||
"register handlers after child components have already fired their events during initialization.",
|
||||
'high'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -865,7 +865,7 @@ LIFECYCLE EVENT CALLBACKS
|
||||
(useful for re-renders)
|
||||
- Multiple callbacks: Can register multiple for same event
|
||||
- Chaining: Returns this so you can chain .on() calls
|
||||
- Only lifecycle events allowed: Other event names log error
|
||||
- Custom events also supported (see CUSTOM COMPONENT EVENTS)
|
||||
|
||||
Example - Wait for component initialization:
|
||||
// Initialize nested components after parent ready
|
||||
@@ -881,6 +881,72 @@ LIFECYCLE EVENT CALLBACKS
|
||||
|
||||
Available in JQHTML v2.2.81+
|
||||
|
||||
CUSTOM COMPONENT EVENTS
|
||||
Components can fire and listen to custom events using the jqhtml event
|
||||
bus. Unlike jQuery's .trigger()/.on(), the jqhtml event bus guarantees
|
||||
callback execution even if the event fired before the handler was
|
||||
registered. This is critical for component lifecycle coordination.
|
||||
|
||||
Firing Events:
|
||||
// Fire event from within a component
|
||||
this.trigger('my_event');
|
||||
this.trigger('my_event', { key: 'value' });
|
||||
|
||||
Listening to Events:
|
||||
|
||||
From parent component (using $sid):
|
||||
// In parent's on_ready()
|
||||
this.sid('child_component').on('my_event', (component, data) => {
|
||||
console.log('Event from:', component);
|
||||
console.log('Event data:', data);
|
||||
});
|
||||
|
||||
From external code:
|
||||
$('#element').component().on('my_event', (component, data) => {
|
||||
// Handle event
|
||||
});
|
||||
|
||||
Callback Signature:
|
||||
.on('event_name', (component, data) => { ... })
|
||||
|
||||
- component: The component instance that fired the event
|
||||
- data: Optional data passed as second argument to trigger()
|
||||
|
||||
Key Difference from jQuery Events:
|
||||
jQuery events are lost if fired before handler registration:
|
||||
|
||||
// jQuery - BROKEN: Event fires before handler exists
|
||||
child.$.trigger('initialized'); // Lost!
|
||||
parent.$sid('child').on('initialized', handler); // Never called
|
||||
|
||||
JQHTML events work regardless of timing:
|
||||
|
||||
// JQHTML - WORKS: Event queued, fires when handler registers
|
||||
child.trigger('initialized'); // Queued
|
||||
parent.sid('child').on('initialized', handler); // Called immediately!
|
||||
|
||||
This enables reliable parent-child communication during component
|
||||
initialization, when event timing is unpredictable.
|
||||
|
||||
Example - Child notifies parent of state change:
|
||||
|
||||
// Child component (Tab_Bar.js)
|
||||
set_active_tab(key) {
|
||||
this.state.active_tab = key;
|
||||
this.render();
|
||||
this.trigger('tab:change', { key: key });
|
||||
}
|
||||
|
||||
// Parent component (Settings_Page.js)
|
||||
on_ready() {
|
||||
this.sid('tabs').on('tab:change', (component, data) => {
|
||||
this.show_panel(data.key);
|
||||
});
|
||||
}
|
||||
|
||||
IMPORTANT: Do not use jQuery's .trigger() for custom events in jqhtml
|
||||
components. The code quality rule JQHTML-EVENT-01 enforces this.
|
||||
|
||||
$REDRAWABLE ATTRIBUTE - LIGHTWEIGHT COMPONENTS
|
||||
Convert any HTML element into a re-renderable component using the
|
||||
$redrawable attribute. This parser-level transformation enables
|
||||
|
||||
Reference in New Issue
Block a user