Add <%br= %> jqhtml syntax docs, class override detection, npm update

Document event handler placement and model fetch clarification

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2026-01-15 10:16:06 +00:00
parent 61f8f058f2
commit 1594502cb2
791 changed files with 7044 additions and 6089 deletions

View File

@@ -310,6 +310,31 @@ class _Manifest_Quality_Helper
// Valid override: exactly one file in rsx/, rest in app/RSpade/
if (count($rsx_files) === 1 && count($framework_files) >= 1) {
$rsx_file = array_values($rsx_files)[0];
$rsx_metadata = Manifest::$data['data']['files'][$rsx_file] ?? [];
$rsx_extends = $rsx_metadata['extends'] ?? null;
// Check if rsx/ class extends the framework class (wrong pattern)
// This happens when someone tries to use OOP inheritance instead of
// the correct RSX override pattern (copy and replace)
if ($rsx_extends === $class_name) {
$framework_file = array_values($framework_files)[0];
throw new \RuntimeException(
"Fatal: Invalid class override pattern for '{$class_name}'.\n\n" .
"The file {$rsx_file} extends the framework class {$class_name},\n" .
"but RSX requires unique class names - you cannot have two classes\n" .
"with the same name, even if one extends the other.\n\n" .
"CORRECT OVERRIDE PATTERN:\n" .
" 1. Copy the framework file to your rsx/ directory:\n" .
" cp system/{$framework_file} {$rsx_file}\n\n" .
" 2. Customize the copy as needed (change namespace, add methods, etc.)\n\n" .
" 3. The framework will automatically detect this and rename the\n" .
" original to .upstream, allowing your version to take over.\n\n" .
"This pattern replaces the framework class entirely, allowing full\n" .
"customization while maintaining the same class name for compatibility."
);
}
$did_change = false;
// Rename framework files to .upstream and remove from manifest
foreach ($framework_files as $framework_file) {

View File

@@ -42,6 +42,7 @@ CONVERSION PROCESS
Variable Interpolation:
{{ $var }} -> <%= this.args.var %> or <%= this.data.var %>
{!! $html !!} -> <%!= this.data.html %>
{{ nl2br($text) }} -> <%br= this.data.text %> (escaped + newlines to <br />)
Control Structures:
@if($cond) / @endif -> <% if (cond) { %> / <% } %>

View File

@@ -81,9 +81,9 @@ FILE-DROP EVENT
Component Event Handler:
class My_Upload_Widget extends Jqhtml_Component {
on_render() {
on_create() {
this.on('file-drop', (component, data) => {
this._handle_files(data.files);
this._handle_files(Array.from(data.files));
});
}
@@ -109,6 +109,55 @@ FILE-DROP EVENT
originalEvent: DragEvent // Original browser event
}
EVENT HANDLER REGISTRATION
IMPORTANT: Register file-drop handlers in on_create(), NOT on_render()
or on_ready().
Component events (this.on()) attach to the component element itself
(this.$), which persists across re-renders. These handlers should be
registered once in on_create().
DOM events on child elements ($sid elements) are recreated on each
render, so those handlers belong in on_render() or on_ready().
Correct - component event in on_create():
on_create() {
// Component event - attach once, persists across re-renders
this.on('file-drop', (component, data) => {
this._handle_files(Array.from(data.files));
});
}
on_render() {
// DOM events on children - must re-attach after each render
this.$sid('clear_btn').on('click', () => this._clear_files());
}
Wrong (causes infinite loop if handler triggers render/reload):
on_ready() {
this.on('file-drop', (component, data) => {
this._handle_files(Array.from(data.files));
});
}
Why This Happens:
The framework's component event system replays events that fired
before handler registration. If you register in on_ready() and
the handler triggers render() or reload():
1. Files are dropped, file-drop event fires
2. Handler not registered yet (on_ready() hasn't run)
3. Event is queued for replay
4. on_ready() runs, calls this.on('file-drop', ...)
5. Queued event replays immediately
6. Handler calls render() or reload()
7. on_ready() runs again, re-registers handler
8. Event replays again → infinite loop
Rule: Component events (this.on()) go in on_create(). Child DOM events
go in on_render() or on_ready().
FILE VALIDATION
Each widget is responsible for validating dropped files. Droppable
@@ -195,7 +244,7 @@ EXAMPLES
</Define:Image_Uploader>
class Image_Uploader extends Jqhtml_Component {
on_render() {
on_create() {
this.on('file-drop', (_, data) => {
const file = data.files[0];
if (!file || !file.type.startsWith('image/')) {
@@ -227,11 +276,9 @@ EXAMPLES
class Document_Dropzone extends Jqhtml_Component {
on_create() {
this.state.files = [];
}
on_render() {
this.on('file-drop', (_, data) => {
for (let file of data.files) {
for (let file of Array.from(data.files)) {
if (!this._validate(file)) continue;
this.state.files.push(file);
this._add_to_list(file);
@@ -295,7 +342,12 @@ TROUBLESHOOTING
Files not being received:
- Verify element has rsx-droppable class
- Check element is visible (not display:none)
- Ensure event handler is registered before drop
- Ensure event handler is registered in on_create()
Infinite loop when dropping files:
- Handler registered in on_ready() or on_render() instead of on_create()
- Handler calls render() or reload(), re-triggering event replay
- Solution: Move this.on('file-drop', ...) to on_create()
Visual feedback not appearing:
- Define CSS for .rsx-drop-active and .rsx-drop-target

View File

@@ -403,16 +403,18 @@ DRAG-AND-DROP UPLOADS WITH DROPPABLE
JavaScript (My_Uploader.js):
class My_Uploader extends Jqhtml_Component {
on_render() {
// Handle dropped files via Droppable
on_create() {
// Component event - register once, persists across re-renders
this.on('file-drop', (_, data) => {
this._upload_files(data.files);
this._upload_files(Array.from(data.files));
});
}
// Handle click-to-upload
on_render() {
// DOM events on children - re-attach after each render
this.$.on('click', () => this.$sid('file_input').click());
this.$sid('file_input').on('change', (e) => {
this._upload_files(e.target.files);
this._upload_files(Array.from(e.target.files));
});
}

View File

@@ -93,9 +93,14 @@ TEMPLATE SYNTAX
Template expressions:
<%= expression %> - Escaped HTML output (safe, default)
<%!= expression %> - Unescaped raw output (pre-sanitized content only)
<%br= expression %> - Escaped output with newlines converted to <br /> tags
<% statement; %> - JavaScript statements (loops, conditionals)
<%-- comment --%> - JQHTML comments (not HTML <!-- --> comments)
The <%br= %> syntax is useful for displaying multi-line text (like notes
or descriptions) where you want to preserve line breaks without allowing
arbitrary HTML. It escapes HTML first, then converts \n to <br />.
Attributes:
$sid="name" - Scoped ID (becomes id="name:component_id")
$attr=value - Component parameter (becomes this.args.attr)
@@ -970,6 +975,51 @@ CUSTOM COMPONENT EVENTS
IMPORTANT: Do not use jQuery's .trigger() for custom events in jqhtml
components. The code quality rule JQHTML-EVENT-01 enforces this.
EVENT HANDLER PLACEMENT
Where to register event handlers depends on what you're attaching to:
Component Events (this.on()) - Register in on_create():
Component events attach to this.$ which persists across re-renders.
Register once in on_create() to avoid infinite loops.
on_create() {
// Component event - register once
this.on('file-drop', (_, data) => this._handle(data));
this.on('custom-event', (_, data) => this._process(data));
}
DANGER: If registered in on_ready() and the handler triggers
render()/reload(), the framework's event replay mechanism causes
an infinite loop (event fires → handler runs → re-render →
on_ready() re-registers → event replays → infinite loop).
Child Component Events (this.sid().on()) - Register in on_ready():
Listening to events from child components is safe in on_ready()
because the handler is on the child, not this component.
on_ready() {
// Child component events - safe in on_ready()
this.sid('tabs').on('tab:change', (_, data) => {
this._show_panel(data.key);
});
}
DOM Events on Child Elements - Register in on_render() or on_ready():
Child DOM elements are recreated on each render, so their handlers
must be re-attached after each render.
on_render() {
// DOM events on children - must re-attach after render
this.$sid('save_btn').on('click', () => this._save());
this.$sid('filter').on('change', () => this._filter());
}
Summary:
this.on('event', ...) → on_create() (component events)
this.sid('child').on('event') → on_ready() (child component events)
this.$sid('elem').on('click') → on_render() (child DOM events)
$REDRAWABLE ATTRIBUTE - LIGHTWEIGHT COMPONENTS
Convert any HTML element into a re-renderable component using the
$redrawable attribute. This parser-level transformation enables