Reorganize RSpade directory structure for clarity
Improve Jqhtml_Integration.js documentation with hydration system explanation Add jqhtml-laravel integration packages for traditional Laravel projects 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
565
node_modules/@jqhtml/core/README.md
generated
vendored
565
node_modules/@jqhtml/core/README.md
generated
vendored
@@ -1,565 +1,24 @@
|
||||
# @jqhtml/core
|
||||
|
||||
Core runtime library for JQHTML v2 - jQuery-first component framework.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @jqhtml/core jquery
|
||||
```
|
||||
|
||||
## Module Structure
|
||||
|
||||
The core package exports two separate entry points:
|
||||
|
||||
### Main Runtime (Production)
|
||||
```javascript
|
||||
import { Component, jqhtml } from '@jqhtml/core';
|
||||
```
|
||||
|
||||
### Debug Utilities (Development Only)
|
||||
```javascript
|
||||
import { showDebugOverlay, hideDebugOverlay } from '@jqhtml/core/debug';
|
||||
```
|
||||
|
||||
The debug module is kept separate to avoid including debug code in production bundles. It provides:
|
||||
- Visual debug overlay with performance profiling
|
||||
- Component flash visualization
|
||||
- Lifecycle logging
|
||||
- Slow render detection
|
||||
|
||||
## Overview
|
||||
|
||||
The core package provides:
|
||||
- Component base class with 5-stage lifecycle
|
||||
- Lifecycle manager for phase coordination
|
||||
- Component registry for dynamic instantiation
|
||||
- Instruction processor for template rendering
|
||||
- jQuery plugin integration
|
||||
- Template rendering with data bindings
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Component
|
||||
|
||||
```javascript
|
||||
import { Component } from '@jqhtml/core';
|
||||
|
||||
class UserCard extends Component {
|
||||
async init() {
|
||||
// Quick setup, hide elements
|
||||
this.$.addClass('loading');
|
||||
}
|
||||
|
||||
async load() {
|
||||
// Fetch data (runs in parallel with siblings)
|
||||
this.data.user = await fetch('/api/user').then(r => r.json());
|
||||
}
|
||||
|
||||
async render() {
|
||||
// Create DOM structure
|
||||
this.$.html(`
|
||||
<div class="card">
|
||||
<h2>${this.data.user.name}</h2>
|
||||
<p>${this.data.user.email}</p>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
async ready() {
|
||||
// Component fully initialized
|
||||
this.$.removeClass('loading');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using jQuery Plugin
|
||||
|
||||
```javascript
|
||||
// Create component on existing element
|
||||
$('#user-container').component(UserCard, { userId: 123 });
|
||||
|
||||
// Get component instance
|
||||
const card = $('#user-container').component();
|
||||
```
|
||||
|
||||
## Component Lifecycle
|
||||
|
||||
Components follow a multi-stage lifecycle with automatic re-rendering:
|
||||
|
||||
1. **constructor** - Instance creation, jQuery element setup (`this.data` is `{}`)
|
||||
2. **render** - Create initial DOM structure (top-down, atomic/synchronous operation)
|
||||
3. **create** - Quick setup after DOM creation (bottom-up, siblings parallel)
|
||||
4. **load** - Fetch data (bottom-up, fully parallel, **ABSOLUTELY NO DOM MODIFICATIONS**)
|
||||
5. **render** (automatic) - Re-render if data changed during load (empties DOM first)
|
||||
6. **create** (automatic) - Re-setup if component was re-rendered
|
||||
7. **ready** - Fully initialized (bottom-up, siblings parallel)
|
||||
8. **destroy** - Cleanup when removed
|
||||
|
||||
### Critical: NO DOM Modifications in load()
|
||||
|
||||
**The requirement that `on_load()` must not modify the DOM is ABSOLUTE.** This is not a guideline but a strict architectural requirement. Violating this will cause rendering issues and race conditions.
|
||||
|
||||
**Data Initialization Timeline:**
|
||||
- Before `load()`: `this.data = {}` (empty object)
|
||||
- After `load()`: `this.data` contains fetched data (remains `{}` if no data loaded)
|
||||
|
||||
### Correct Patterns for Loading States
|
||||
|
||||
If you need to show different DOM states before and after loading, use these patterns:
|
||||
|
||||
**Pattern 1: Conditional Rendering in Template**
|
||||
```jqhtml
|
||||
<Define:UserProfile>
|
||||
<div class="user-profile">
|
||||
<% if (Object.keys(this.data).length === 0): %>
|
||||
<!-- Show loading state (no data loaded yet) -->
|
||||
<div class="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Loading user profile...</p>
|
||||
</div>
|
||||
<% else: %>
|
||||
<!-- Show loaded content -->
|
||||
<h2><%= this.data.user?.name || 'Unknown User' %></h2>
|
||||
<p><%= this.data.user?.bio || 'No bio available' %></p>
|
||||
<% endif; %>
|
||||
</div>
|
||||
</Define:UserProfile>
|
||||
```
|
||||
|
||||
**Pattern 2: DOM Changes in create() and ready()**
|
||||
```javascript
|
||||
class DataComponent extends Component {
|
||||
async on_create() {
|
||||
// Set loading state in the DOM during create phase
|
||||
this.$sid('status').addClass('loading').text('Loading...');
|
||||
}
|
||||
|
||||
async on_load() {
|
||||
// ONLY fetch data - NO DOM modifications here!
|
||||
this.data.user = await fetch('/api/user').then(r => r.json());
|
||||
}
|
||||
|
||||
async on_ready() {
|
||||
// Update DOM after data is loaded
|
||||
this.$sid('status').removeClass('loading').text('Loaded');
|
||||
this.$sid('username').text(this.data.user.name);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**NEVER do this:**
|
||||
```javascript
|
||||
class BadComponent extends Component {
|
||||
async on_load() {
|
||||
// ❌ WRONG - DOM modification in load()
|
||||
this.$sid('status').text('Loading...'); // VIOLATION!
|
||||
|
||||
this.data.user = await fetch('/api/user').then(r => r.json());
|
||||
|
||||
// ❌ WRONG - More DOM modification
|
||||
this.$sid('status').text('Loaded'); // VIOLATION!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Important:** The `render()` method is atomic and essentially synchronous - it empties the DOM and rebuilds it in a single operation. While marked `async` for technical reasons, it completes immediately without yielding control.
|
||||
|
||||
### Phase Batching
|
||||
|
||||
All components complete each phase before moving to the next:
|
||||
- Parents render before children
|
||||
- Children create/load/ready before parents
|
||||
- Siblings execute in parallel where safe
|
||||
|
||||
### Automatic Re-rendering
|
||||
|
||||
After the `load` phase, if `this.data` has changed, the component automatically:
|
||||
1. Calls `render()` again (which empties the DOM with `$.empty()` first)
|
||||
2. Calls `create()` again to re-setup the new DOM
|
||||
3. Then proceeds to `ready()`
|
||||
|
||||
This ensures components can fetch data and re-render with that data seamlessly.
|
||||
|
||||
### Controlling Re-rendering
|
||||
|
||||
By default, components automatically re-render if `this.data` changes during `load()`:
|
||||
|
||||
```javascript
|
||||
class DataComponent extends Component {
|
||||
async on_load() {
|
||||
// If this modifies this.data, component will re-render
|
||||
this.data.user = await fetch('/api/user').then(r => r.json());
|
||||
}
|
||||
|
||||
// Override to customize re-render logic
|
||||
should_rerender() {
|
||||
// Default: returns true if JSON.stringify(this.data) changed
|
||||
// Override for custom logic:
|
||||
return this.data.user && !this.data.cached;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Manual Re-rendering
|
||||
|
||||
You can manually re-render a component at any time:
|
||||
|
||||
```javascript
|
||||
class InteractiveComponent extends Component {
|
||||
async handleUpdate(newData) {
|
||||
// Update data
|
||||
this.data = { ...this.data, ...newData };
|
||||
|
||||
// Manually re-render (atomic operation)
|
||||
await this.render();
|
||||
|
||||
// Optionally re-initialize (call create/ready as needed)
|
||||
await this.on_create(); // Re-setup event handlers
|
||||
await this.on_ready(); // Re-initialize component state
|
||||
}
|
||||
|
||||
// Lifecycle manipulation methods
|
||||
redraw() {
|
||||
// Synchronously re-renders with current data
|
||||
}
|
||||
|
||||
async reload_data() {
|
||||
// Re-fetches data via on_load(), then redraws
|
||||
}
|
||||
|
||||
async reinitialize() {
|
||||
// Full lifecycle reset from stage 0
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// Cleanup component and children
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** The re-render process:
|
||||
1. Empties the component's DOM with `this.$.empty()`
|
||||
2. Calls `render()` to rebuild the DOM atomically
|
||||
3. For automatic re-renders, calls `create()` again for the new DOM
|
||||
4. Finally calls `ready()`
|
||||
5. For manual re-renders, you control which lifecycle methods to call
|
||||
|
||||
## Scoped IDs
|
||||
|
||||
Components use `_cid` for scoped element selection:
|
||||
|
||||
```javascript
|
||||
class TabsComponent extends Component {
|
||||
async render() {
|
||||
this.$.html(`
|
||||
<button id="tab1:${this._cid}">Tab 1</button>
|
||||
<button id="tab2:${this._cid}">Tab 2</button>
|
||||
<div id="content:${this._cid}"></div>
|
||||
`);
|
||||
}
|
||||
|
||||
selectTab(tabId) {
|
||||
// Use $sid() for scoped selection
|
||||
this.$sid('tab1').removeClass('active');
|
||||
this.$sid('tab2').removeClass('active');
|
||||
this.$sid(tabId).addClass('active');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Template Integration
|
||||
|
||||
The core runtime processes templates compiled by `@jqhtml/parser`:
|
||||
|
||||
```javascript
|
||||
import { render_template, with_template } from '@jqhtml/core';
|
||||
|
||||
// Template function (usually generated by parser)
|
||||
const template = function(data, args, content) {
|
||||
const _output = [];
|
||||
_output.push({tag: ["h1", {"data-bind-text": "title"}, false]});
|
||||
_output.push({tag: ["h1", {}, true]});
|
||||
return [_output, this];
|
||||
};
|
||||
|
||||
// Component with template
|
||||
class Article extends Component {
|
||||
async on_render() {
|
||||
await render_template(this, template);
|
||||
}
|
||||
}
|
||||
|
||||
// Or use mixin
|
||||
const Article = with_template(template)(Component);
|
||||
```
|
||||
|
||||
## Attribute Rules
|
||||
|
||||
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: `$sid="my-id"` or `$data=this.complexObject`
|
||||
- **Regular HTML attributes**: MUST be quoted (strings only)
|
||||
Example: `class="container <%= this.args.theme %>"`
|
||||
|
||||
See [ATTRIBUTE_RULES.md](../../docs/ATTRIBUTE_RULES.md) for comprehensive documentation.
|
||||
|
||||
## Data Bindings
|
||||
|
||||
Templates support reactive data bindings:
|
||||
|
||||
- `:text="expression"` - Update text content
|
||||
- `:value="expression"` - Form input values
|
||||
- `:class="{active: isActive}"` - Dynamic classes
|
||||
- `:style="{color: textColor}"` - Dynamic styles
|
||||
- `@click=handler` - Event handlers (unquoted)
|
||||
|
||||
## Browser Bundle
|
||||
|
||||
For browser usage without a build tool:
|
||||
A jQuery-based component framework for people who think in systems, not pixels.
|
||||
|
||||
```html
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||
<script src="jqhtml-bundle.js"></script>
|
||||
<script>
|
||||
const { Component } = JQHTML;
|
||||
|
||||
class MyComponent extends Component {
|
||||
// ...
|
||||
}
|
||||
</script>
|
||||
<Define:User_Card class="card">
|
||||
<h3><%= this.data.name %></h3>
|
||||
<p><%= this.data.email %></p>
|
||||
</Define:User_Card>
|
||||
```
|
||||
|
||||
## Lifecycle Event Callbacks
|
||||
Components have a simple lifecycle: `on_load()` fetches data, `on_render()` sets up the DOM, `on_ready()` fires when everything's ready. No virtual DOM, no complex state management - just jQuery under the hood.
|
||||
|
||||
JQHTML provides a `.on()` method for registering callbacks that fire after lifecycle events complete. This is useful for external code that needs to know when a component reaches a certain state.
|
||||
**vs React/Vue:** JQHTML is for when you want component structure without the SPA complexity. Great for server-rendered apps (Laravel, Rails, Django) where you need interactive islands, not a full frontend framework.
|
||||
|
||||
### Supported Events
|
||||
## Status
|
||||
|
||||
- `'render'` - Fires after the render phase completes
|
||||
- `'create'` - Fires after the create phase completes
|
||||
- `'load'` - Fires after the load phase completes
|
||||
- `'ready'` - Fires after the ready phase completes (component fully initialized)
|
||||
Alpha release. It works and I use it daily, but expect rough edges. Full documentation and framework plugins coming soon.
|
||||
|
||||
### Usage
|
||||
If you try it in a project, I'd love to hear about it.
|
||||
|
||||
```javascript
|
||||
// Get component instance and register callback
|
||||
const component = $('#my-component').component();
|
||||
---
|
||||
|
||||
component.on('ready', (comp) => {
|
||||
console.log('Component is ready!', comp);
|
||||
// Access component data, DOM, etc.
|
||||
});
|
||||
|
||||
// If the event already occurred, callback fires immediately
|
||||
// AND still registers for future occurrences (e.g., on re-render)
|
||||
```
|
||||
|
||||
### Behavior
|
||||
|
||||
1. **Immediate Execution**: If the lifecycle event has already occurred when you register the callback, it fires immediately
|
||||
2. **Future Events**: The callback also registers for future occurrences of the same event (useful for re-renders)
|
||||
3. **Multiple Callbacks**: You can register multiple callbacks for the same event
|
||||
4. **Error Safety**: Errors in callbacks are caught and logged without breaking the component
|
||||
5. **Validation**: Only lifecycle events are allowed; other event names trigger a console error
|
||||
|
||||
### Example: Wait for Component to be Ready
|
||||
|
||||
```javascript
|
||||
// Register callback before or after component initialization
|
||||
$('#user-profile').component().on('ready', (component) => {
|
||||
// Component is fully initialized, data is loaded
|
||||
console.log('User data:', component.data.user);
|
||||
|
||||
// Safe to access all DOM elements
|
||||
component.$sid('email').addClass('verified');
|
||||
});
|
||||
```
|
||||
|
||||
### Example: Track Multiple Lifecycle Events
|
||||
|
||||
```javascript
|
||||
const component = $('#dashboard').component();
|
||||
|
||||
component
|
||||
.on('render', () => console.log('Dashboard rendered'))
|
||||
.on('create', () => console.log('Dashboard created'))
|
||||
.on('load', () => console.log('Dashboard data loaded'))
|
||||
.on('ready', () => console.log('Dashboard ready'));
|
||||
```
|
||||
|
||||
### Example: Invalid Event (Error)
|
||||
|
||||
```javascript
|
||||
// This will log an error to console
|
||||
$('#my-component').component().on('click', callback);
|
||||
// Error: Component.on() only supports lifecycle events: render, create, load, ready
|
||||
```
|
||||
|
||||
### Note on Re-renders
|
||||
|
||||
When a component re-renders (manually or automatically after `load()`), lifecycle events fire again:
|
||||
|
||||
```javascript
|
||||
const component = $('#widget').component();
|
||||
|
||||
component.on('render', () => {
|
||||
console.log('Widget rendered');
|
||||
// Fires on initial render AND every re-render
|
||||
});
|
||||
|
||||
// Later, trigger re-render
|
||||
await component.render(); // "Widget rendered" logs again
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Component Class
|
||||
|
||||
- `constructor(element, args)` - Create component instance
|
||||
- `async render()` - Create DOM structure
|
||||
- `async init()` - Quick setup phase
|
||||
- `async load()` - Data fetching phase
|
||||
- `async ready()` - Final initialization
|
||||
- `destroy()` - Cleanup
|
||||
- `should_rerender()` - Control re-rendering after load
|
||||
- `emit(event, data)` - Emit jQuery events
|
||||
- `on(event, callback)` - Register lifecycle event callback
|
||||
- `$sid(localId)` - Get scoped jQuery element
|
||||
- `id(localId)` - Get scoped component instance
|
||||
- `parent()` - Get parent component
|
||||
- `children()` - Get direct child components
|
||||
- `find(selector)` - Find descendant components
|
||||
|
||||
### jQuery Plugin
|
||||
|
||||
- `$(el).component()` - Get component instance
|
||||
- `$(el).component(Class, args)` - Create component
|
||||
- `$.jqhtml.register(name, Class)` - Register component
|
||||
- `$.jqhtml.create(name, args)` - Create by name
|
||||
|
||||
### Template Functions
|
||||
|
||||
- `render_template(component, template)` - Render template
|
||||
- `with_template(template)` - Component mixin
|
||||
- `process_instructions(instructions, target, context)` - Process instruction array
|
||||
|
||||
## Component and Template Registration Flexibility
|
||||
|
||||
JQHTML provides complete flexibility in how you define components - you can use JavaScript classes, `.jqhtml` template files, both, or neither. The framework automatically handles all combinations:
|
||||
|
||||
### All Possible Combinations
|
||||
|
||||
1. **Both JS Class and .jqhtml Template** (Standard approach)
|
||||
```javascript
|
||||
// UserCard.js
|
||||
class UserCard extends Jqhtml_Component {
|
||||
async load() {
|
||||
this.data.user = await fetch('/api/user').then(r => r.json());
|
||||
}
|
||||
}
|
||||
jqhtml.register_component('UserCard', UserCard);
|
||||
```
|
||||
```html
|
||||
<!-- UserCard.jqhtml -->
|
||||
<Define:UserCard>
|
||||
<div class="card">
|
||||
<h2><%= this.data.user.name %></h2>
|
||||
</div>
|
||||
</Define:UserCard>
|
||||
```
|
||||
**Result**: Uses your custom class logic + your custom template
|
||||
|
||||
2. **.jqhtml Template Only** (No JS class needed)
|
||||
```html
|
||||
<!-- SimpleWidget.jqhtml -->
|
||||
<Define:SimpleWidget>
|
||||
<div class="widget">
|
||||
Hello, <%= this.args.name %>!
|
||||
</div>
|
||||
</Define:SimpleWidget>
|
||||
```
|
||||
**Result**: Uses default `Jqhtml_Component` class + your template. Perfect for presentational components that don't need custom logic.
|
||||
|
||||
3. **JS Class Only** (No .jqhtml template)
|
||||
```javascript
|
||||
class DynamicComponent extends Jqhtml_Component {
|
||||
async render() {
|
||||
this.$.html(`<div>Rendered at ${Date.now()}</div>`);
|
||||
}
|
||||
}
|
||||
jqhtml.register_component('DynamicComponent', DynamicComponent);
|
||||
```
|
||||
**Result**: Uses your class + default passthrough template. Useful when you want full programmatic control.
|
||||
|
||||
4. **Neither Class nor Template** (Passthrough)
|
||||
```html
|
||||
<!-- In your JQHTML file -->
|
||||
<FooBar>
|
||||
<p>This content just passes through!</p>
|
||||
</FooBar>
|
||||
```
|
||||
**Result**: Uses default `Jqhtml_Component` class + default passthrough template. Acts as a simple wrapper that renders its inner content.
|
||||
|
||||
### How It Works
|
||||
|
||||
When you use `<ComponentName>`:
|
||||
- **JS Class Resolution**: Looks for registered class, falls back to `Jqhtml_Component`
|
||||
- **Template Resolution**: Looks for registered template by class, then by name, then uses default passthrough
|
||||
- **Default Passthrough Template**: Simply renders the component's inner HTML as-is
|
||||
|
||||
This means you can start simple and progressively add complexity:
|
||||
```html
|
||||
<!-- Start with just markup -->
|
||||
<UserBadge>Basic HTML content</UserBadge>
|
||||
|
||||
<!-- Later, add a .jqhtml template for consistent styling -->
|
||||
<Define:UserBadge>
|
||||
<div class="badge"><%= this.args.username %></div>
|
||||
</Define:UserBadge>
|
||||
|
||||
<!-- Finally, add JS class for complex behavior -->
|
||||
class UserBadge extends Jqhtml_Component {
|
||||
async load() { /* fetch user data */ }
|
||||
}
|
||||
```
|
||||
|
||||
## Laravel Integration
|
||||
|
||||
This package includes a Laravel bridge for error handling and source map support. After installing via npm, you can load it directly from `node_modules` in your Laravel application:
|
||||
|
||||
```php
|
||||
// In app/Providers/AppServiceProvider.php
|
||||
public function register()
|
||||
{
|
||||
$jqhtmlBridge = base_path('node_modules/@jqhtml/core/laravel-bridge/autoload.php');
|
||||
if (file_exists($jqhtmlBridge)) {
|
||||
require_once $jqhtmlBridge;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See [laravel-bridge/LARAVEL_INTEGRATION.md](./laravel-bridge/LARAVEL_INTEGRATION.md) for complete integration instructions.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build TypeScript
|
||||
npm run build
|
||||
|
||||
# Run tests
|
||||
npm test
|
||||
|
||||
# Watch mode
|
||||
npm run watch
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
**hansonxyz** · [hanson.xyz](https://hanson.xyz/) · [github](https://github.com/hansonxyz)
|
||||
|
||||
Reference in New Issue
Block a user