Files
rspade_system/node_modules/@jqhtml/core/README.md
root 78553d4edf 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>
2025-11-21 04:35:01 +00:00

16 KiB
Executable File

@jqhtml/core

Core runtime library for JQHTML v2 - jQuery-first component framework.

Installation

npm install @jqhtml/core jquery

Module Structure

The core package exports two separate entry points:

Main Runtime (Production)

import { Component, jqhtml } from '@jqhtml/core';

Debug Utilities (Development Only)

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

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

// 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

<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()

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:

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():

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:

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:

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 $id() 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:

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 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:

<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>

Lifecycle Event Callbacks

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.

Supported Events

  • '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)

Usage

// 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

// 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

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)

// 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:

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
  • $id(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)

    // 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);
    
    <!-- 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)

    <!-- 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)

    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)

    <!-- 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:

<!-- 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:

// 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 for complete integration instructions.

Development

# Install dependencies
npm install

# Build TypeScript
npm run build

# Run tests
npm test

# Watch mode
npm run watch

License

MIT