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:
@@ -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) {
|
||||
|
||||
@@ -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) { %> / <% } %>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user