Skip to content

metabench/jsgui3-html

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Jsgui3 HTML

Jsgui3-html is an isomorphic (server and client-side) UI component framework that provides a comprehensive control system for building dynamic web applications. It emphasizes compositional architecture, state management, and seamless rendering across environments.

Table of Contents

Core Concepts

Controls

Controls are the fundamental building blocks of jsgui3-html applications. They are analogous to React Components but designed with a focus on:

  • Isomorphic operation: Work identically on server and client
  • Compositional architecture: Build complex UIs from simple components
  • State separation: Distinguish between data models and view models
  • Direct DOM manipulation: No virtual DOM - direct, efficient updates

Evented Architecture

Built on the Evented_Class from the lang-tools package, controls can:

  • Listen to and raise custom events
  • Respond to DOM events
  • Communicate between parent and child controls
  • Handle data model changes reactively

Compositional Model

Controls use a compositional model where:

  • Complex controls are assembled from simpler subcontrols
  • Layout and behavior are defined declaratively
  • Dynamic updates are handled automatically
  • Reusable patterns can be extracted as mixins

Architecture Overview

Control_Core
β”œβ”€β”€ DOM Management (Control_DOM, DOM_Attributes)
β”œβ”€β”€ Event Handling (Evented_Class)
β”œβ”€β”€ Rendering (HTML generation)
└── Content Management (Collection)

Control (extends Control_Core)
β”œβ”€β”€ Data Binding
β”œβ”€β”€ View Management (Control_View)
β”œβ”€β”€ Compositional Model Support
└── Enhanced Event Mapping

Data_Model_View_Model_Control (extends Control)
β”œβ”€β”€ Separate Data and View Models
β”œβ”€β”€ Automatic Synchronization
β”œβ”€β”€ State Persistence
└── Complex Data Structure Support

Key Classes

Class Purpose
Control_Core Base class providing DOM manipulation, events, and rendering
Control Enhanced controls with data binding and compositional models
Data_Model_View_Model_Control Controls with explicit data/view model separation
Control_View Manages visual representation and UI state
Control_DOM Handles DOM-specific functionality and attributes
DOM_Attributes Manages DOM attributes with reactive updates

Control Lifecycle

1. Construction

const button = new Control({
    tagName: 'button',
    text: 'Click me',
    class: 'primary-btn'
});

2. Composition

Controls build their internal structure:

compose() {
    this.add(new Icon({ name: 'check' }));
    this.add(new Text({ value: this.label }));
}

3. Rendering (Server-side)

Generate HTML for initial page load:

const html = control.all_html_render();
// <button data-jsgui-id="ctrl_123" class="primary-btn">
//   <i class="icon-check"></i>Click me
// </button>

4. Activation (Client-side)

Connect rendered HTML to control instances:

control.activate(); // Binds to existing DOM element

5. Event Responses

Handle user interactions and data changes:

control.on('click', () => {
    this.data.model.count++;
});

State Management

Data Models vs View Models

Data Model: Contains raw, business logic data

this.data.model = new Data_Object({
    user_id: 123,
    email: '[email protected]',
    created_at: new Date()
});

View Model: Contains UI-specific representations

this.view.data.model = new Data_Object({
    formatted_email: '[email protected]',
    display_date: '2023-09-01',
    is_highlighted: false
});

Automatic Synchronization

Changes in data models can automatically update view models:

this.data.model.on('change', e => {
    if (e.name === 'email') {
        this.view.data.model.formatted_email = formatEmail(e.value);
    }
});

State Persistence

State is serialized into HTML attributes for isomorphic operation:

<div data-jsgui-id="ctrl_123" 
     data-jsgui-fields="{'selected':true,'count':5}"
     data-jsgui-data-model-id="model_456">

Advanced State Management Patterns

Observer Pattern Implementation

// Data model changes automatically propagate
class ObservableModel extends Data_Object {
    constructor(data) {
        super(data);
        this.observers = new Set();
    }
    
    addObserver(callback) {
        this.observers.add(callback);
        return () => this.observers.delete(callback); // Return unsubscribe function
    }
    
    notifyObservers(change) {
        this.observers.forEach(callback => callback(change));
    }
}

Model-View Synchronization

class SyncedControl extends Data_Model_View_Model_Control {
    constructor(spec) {
        super(spec);
        
        // Bidirectional binding helper
        this.bindProperty('user_name', {
            dataToView: (value) => value.toUpperCase(),
            viewToData: (value) => value.toLowerCase(),
            immediate: true // Apply transform immediately
        });
    }
    
    bindProperty(dataProperty, options = {}) {
        const { dataToView, viewToData, immediate } = options;
        
        // Data β†’ View
        this.data.model.on('change', e => {
            if (e.name === dataProperty) {
                const transformed = dataToView ? dataToView(e.value) : e.value;
                this.view.data.model[dataProperty] = transformed;
            }
        });
        
        // View β†’ Data
        this.view.data.model.on('change', e => {
            if (e.name === dataProperty) {
                const transformed = viewToData ? viewToData(e.value) : e.value;
                this.data.model[dataProperty] = transformed;
            }
        });
        
        // Initial sync
        if (immediate && this.data.model[dataProperty] !== undefined) {
            const transformed = dataToView ? dataToView(this.data.model[dataProperty]) : this.data.model[dataProperty];
            this.view.data.model[dataProperty] = transformed;
        }
    }
}

Complex Form Validation

class ValidationManager {
    constructor(control) {
        this.control = control;
        this.rules = new Map();
        this.errors = new Map();
    }
    
    addRule(field, validator) {
        if (!this.rules.has(field)) {
            this.rules.set(field, []);
        }
        this.rules.get(field).push(validator);
        
        // Auto-validate on field change
        this.control.data.model.on('change', e => {
            if (e.name === field) {
                this.validateField(field, e.value);
            }
        });
    }
    
    validateField(field, value) {
        const fieldRules = this.rules.get(field) || [];
        const fieldErrors = [];
        
        for (const rule of fieldRules) {
            const result = rule(value);
            if (result !== true) {
                fieldErrors.push(result);
            }
        }
        
        if (fieldErrors.length > 0) {
            this.errors.set(field, fieldErrors);
        } else {
            this.errors.delete(field);
        }
        
        // Update view model
        this.control.view.data.model[`${field}_errors`] = fieldErrors;
        this.control.view.data.model[`${field}_valid`] = fieldErrors.length === 0;
        
        return fieldErrors.length === 0;
    }
    
    validateAll() {
        let isValid = true;
        this.rules.forEach((rules, field) => {
            const fieldValue = this.control.data.model[field];
            const fieldValid = this.validateField(field, fieldValue);
            isValid = isValid && fieldValid;
        });
        return isValid;
    }
}

// Usage
class RegistrationForm extends Data_Model_View_Model_Control {
    constructor(spec) {
        super(spec);
        
        this.validator = new ValidationManager(this);
        
        // Add validation rules
        this.validator.addRule('email', value => {
            if (!value) return 'Email is required';
            if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'Invalid email format';
            return true;
        });
        
        this.validator.addRule('password', value => {
            if (!value) return 'Password is required';
            if (value.length < 8) return 'Password must be at least 8 characters';
            return true;
        });
    }
}

Mixins System

Mixins provide reusable functionality that can be applied to any control:

Available Mixins

Mixin Purpose
selectable Adds selection state and UI behavior
dragable Enables drag and drop functionality
press_events Handles press/touch events with timing
pressed_state Visual feedback for press interactions

Using Mixins

const mx_selectable = require('./control_mixins/selectable');

class ListItem extends Control {
    constructor(spec) {
        super(spec);
        mx_selectable(this); // Adds selection capability
    }
}

Creating Custom Mixins

const my_mixin = (ctrl, options = {}) => {
    // Add properties
    ctrl.custom_property = options.value || 'default';
    
    // Add event handlers
    ctrl.on('custom_event', () => {
        // Handle event
    });
    
    // Setup isomorphic behavior
    ctrl.on('server-pre-render', () => {
        ctrl._fields = ctrl._fields || {};
        ctrl._fields.custom_property = ctrl.custom_property;
    });
};

Rendering and DOM Management

HTML Generation

Controls generate HTML strings for server-side rendering:

renderBeginTagToHtml() {
    return `<${this.dom.tagName}${this.renderDomAttributes()}>`;
}

renderEndTagToHtml() {
    return `</${this.dom.tagName}>`;
}

DOM Attributes

Attributes are managed reactively:

this.dom.attrs.class = 'active selected';
this.dom.attrs.style.color = 'red';
// Automatically updates DOM when activated

CSS Management

// Add/remove classes
control.add_class('active');
control.remove_class('disabled');
control.has_class('selected'); // true/false

// Direct style manipulation
control.style('background-color', '#ff0000');
control.style({ width: '100px', height: '50px' });

Event System

DOM Events

Automatic mapping of DOM events to control events:

control.on('click', e => {
    console.log('Button clicked');
});

control.on('change', e => {
    this.data.model.value = e.target.value;
});

Custom Events

Controls can raise and listen to custom events:

// Raise event
control.raise('data_changed', { 
    old_value: prev, 
    new_value: current 
});

// Listen to event
control.on('data_changed', e => {
    this.update_display(e.new_value);
});

Event Delegation

Events bubble up through the control hierarchy:

parent_control.on('child_selected', e => {
    console.log('Child control selected:', e.ctrl_target);
});

Examples

Basic Button with State

class ToggleButton extends Control {
    constructor(spec) {
        super(spec);
        
        this.data.model = new Data_Object({
            active: spec.active || false
        });
        
        this.view.data.model = new Data_Object({
            label: spec.active ? 'ON' : 'OFF'
        });
        
        this.on('click', () => {
            this.data.model.active = !this.data.model.active;
        });
        
        this.data.model.on('change', e => {
            if (e.name === 'active') {
                this.view.data.model.label = e.value ? 'ON' : 'OFF';
                this.toggle_class('active', e.value);
            }
        });
    }
}

Data Grid with Pagination

class DataGrid extends Data_Model_View_Model_Control {
    constructor(spec) {
        super(spec);
        
        this.data.model = new Data_Object({
            records: spec.data || [],
            total_count: spec.total || 0
        });
        
        this.view.data.model = new Data_Object({
            current_page: 1,
            page_size: 10,
            visible_records: []
        });
        
        this.compose_grid();
        this.update_visible_records();
    }
    
    compose_grid() {
        this.add(this.header = new GridHeader({ context: this.context }));
        this.add(this.body = new GridBody({ context: this.context }));
        this.add(this.footer = new GridFooter({ context: this.context }));
    }
}

SVG Rendering

const circle = new Control({
    tagName: 'circle',
    attrs: {
        cx: 50,
        cy: 50,
        r: 40,
        stroke: 'green',
        'stroke-width': 4,
        fill: 'yellow'
    }
});

const svg = new Control({
    tagName: 'svg',
    attrs: {
        width: 100,
        height: 100
    }
});
svg.add(circle);

Form with Validation

class ValidatedInput extends Control {
    constructor(spec) {
        super(spec);
        
        this.data.model = new Data_Object({
            value: '',
            is_valid: true,
            error_message: ''
        });
        
        this.view.data.model = new Data_Object({
            display_value: '',
            show_error: false
        });
        
        this.on('input', e => {
            const value = e.target.value;
            this.data.model.value = value;
            this.validate(value);
        });
    }
    
    validate(value) {
        const is_valid = this.spec.validator ? this.spec.validator(value) : true;
        this.data.model.is_valid = is_valid;
        this.view.data.model.show_error = !is_valid;
    }
}

Real-World Application Patterns

Complete CRUD Application Example

// User management application
class UserManager extends Data_Model_View_Model_Control {
    constructor(spec) {
        super(spec);
        
        // Data model - raw user data from API
        this.data.model = new Data_Object({
            users: [],
            loading: false,
            selected_user: null,
            filter: '',
            sort_by: 'name',
            sort_direction: 'asc'
        });
        
        // View model - UI state and formatted data
        this.view.data.model = new Data_Object({
            filtered_users: [],
            display_mode: 'list', // list, grid, detail
            page: 1,
            per_page: 10,
            show_add_form: false,
            show_delete_confirm: false
        });
        
        this.setup_data_bindings();
        this.compose_interface();
        this.load_users();
    }
    
    setup_data_bindings() {
        // Auto-filter users when filter changes
        this.data.model.on('change', e => {
            if (['users', 'filter', 'sort_by', 'sort_direction'].includes(e.name)) {
                this.update_filtered_users();
            }
        });
        
        // Update pagination when filtered users change
        this.view.data.model.on('change', e => {
            if (e.name === 'filtered_users') {
                this.update_pagination();
            }
        });
    }
    
    compose_interface() {
        // Header with search and controls
        this.header = new Control({
            tagName: 'header',
            class: 'user-manager-header'
        });
        
        this.search_input = new Control({
            tagName: 'input',
            attrs: { 
                type: 'text', 
                placeholder: 'Search users...' 
            }
        });
        
        this.add_button = new Control({
            tagName: 'button',
            text: 'Add User',
            class: 'btn btn-primary'
        });
        
        this.header.add(this.search_input);
        this.header.add(this.add_button);
        
        // User list/grid container
        this.user_container = new Control({
            tagName: 'div',
            class: 'user-container'
        });
        
        // Pagination controls
        this.pagination = new PaginationControl({
            context: this.context
        });
        
        this.add(this.header);
        this.add(this.user_container);
        this.add(this.pagination);
        
        this.setup_event_handlers();
    }
    
    setup_event_handlers() {
        // Search input
        this.search_input.on('input', e => {
            this.data.model.filter = e.target.value;
        });
        
        // Add user button
        this.add_button.on('click', () => {
            this.view.data.model.show_add_form = true;
            this.show_add_user_form();
        });
        
        // User selection
        this.on('user_selected', e => {
            this.data.model.selected_user = e.user;
            this.show_user_detail(e.user);
        });
    }
    
    update_filtered_users() {
        let filtered = [...this.data.model.users];
        
        // Apply filter
        if (this.data.model.filter) {
            const filter = this.data.model.filter.toLowerCase();
            filtered = filtered.filter(user => 
                user.name.toLowerCase().includes(filter) ||
                user.email.toLowerCase().includes(filter)
            );
        }
        
        // Apply sorting
        filtered.sort((a, b) => {
            const field = this.data.model.sort_by;
            const direction = this.data.model.sort_direction === 'asc' ? 1 : -1;
            return a[field].localeCompare(b[field]) * direction;
        });
        
        this.view.data.model.filtered_users = filtered;
        this.render_users();
    }
    
    render_users() {
        this.user_container.clear();
        
        const users = this.view.data.model.filtered_users;
        const start = (this.view.data.model.page - 1) * this.view.data.model.per_page;
        const end = start + this.view.data.model.per_page;
        const page_users = users.slice(start, end);
        
        page_users.forEach(user => {
            const user_item = new UserItem({
                context: this.context,
                user: user
            });
            
            user_item.on('click', () => {
                this.raise('user_selected', { user });
            });
            
            this.user_container.add(user_item);
        });
    }
    
    async load_users() {
        this.data.model.loading = true;
        try {
            const response = await fetch('/api/users');
            const users = await response.json();
            this.data.model.users = users;
        } catch (error) {
            console.error('Failed to load users:', error);
        } finally {
            this.data.model.loading = false;
        }
    }
}

class UserItem extends Control {
    constructor(spec) {
        super(spec);
        this.user = spec.user;
        this.compose_user_item();
    }
    
    compose_user_item() {
        this.dom.tagName = 'div';
        this.add_class('user-item');
        
        this.avatar = new Control({
            tagName: 'img',
            attrs: { 
                src: this.user.avatar || '/default-avatar.png',
                alt: this.user.name
            },
            class: 'user-avatar'
        });
        
        this.info = new Control({
            tagName: 'div',
            class: 'user-info'
        });
        
        this.name = new Control({
            tagName: 'h3',
            text: this.user.name,
            class: 'user-name'
        });
        
        this.email = new Control({
            tagName: 'p',
            text: this.user.email,
            class: 'user-email'
        });
        
        this.info.add(this.name);
        this.info.add(this.email);
        
        this.add(this.avatar);
        this.add(this.info);
    }
}

Progressive Web App (PWA) Integration

class PWAApplication extends Control {
    constructor(spec) {
        super(spec);
        
        this.setup_service_worker();
        this.setup_offline_support();
        this.setup_app_shell();
    }
    
    setup_service_worker() {
        if ('serviceWorker' in navigator) {
            navigator.serviceWorker.register('/sw.js')
                .then(registration => {
                    console.log('SW registered:', registration);
                })
                .catch(error => {
                    console.log('SW registration failed:', error);
                });
        }
    }
    
    setup_offline_support() {
        // Cache critical application state
        this.on('data_change', e => {
            if (e.critical) {
                localStorage.setItem('app_state', JSON.stringify({
                    timestamp: Date.now(),
                    data: e.data
                }));
            }
        });
        
        // Restore state on startup
        window.addEventListener('load', () => {
            const cached_state = localStorage.getItem('app_state');
            if (cached_state) {
                const { data } = JSON.parse(cached_state);
                this.restore_state(data);
            }
        });
    }
    
    setup_app_shell() {
        this.app_shell = new Control({
            tagName: 'div',
            class: 'app-shell'
        });
        
        this.header = new AppHeader({ context: this.context });
        this.nav = new AppNavigation({ context: this.context });
        this.main = new Control({ tagName: 'main', class: 'app-main' });
        this.footer = new AppFooter({ context: this.context });
        
        this.app_shell.add(this.header);
        this.app_shell.add(this.nav);
        this.app_shell.add(this.main);
        this.app_shell.add(this.footer);
        
        this.add(this.app_shell);
    }
}

Multi-Language Support

class I18nControl extends Control {
    constructor(spec) {
        super(spec);
        
        this.locale = spec.locale || 'en';
        this.translations = new Map();
        this.setup_i18n();
    }
    
    setup_i18n() {
        // Load translations
        this.load_translations(this.locale);
        
        // Watch for locale changes
        this.on('locale_change', e => {
            this.locale = e.locale;
            this.load_translations(this.locale);
            this.update_all_text();
        });
    }
    
    async load_translations(locale) {
        try {
            const response = await fetch(`/i18n/${locale}.json`);
            const translations = await response.json();
            this.translations.set(locale, translations);
        } catch (error) {
            console.error(`Failed to load translations for ${locale}:`, error);
        }
    }
    
    t(key, params = {}) {
        const translations = this.translations.get(this.locale) || {};
        let text = translations[key] || key;
        
        // Replace parameters
        Object.entries(params).forEach(([param, value]) => {
            text = text.replace(`{{${param}}}`, value);
        });
        
        return text;
    }
    
    update_all_text() {
        // Update all translatable text in the control tree
        this.iterate_this_and_subcontrols(ctrl => {
            if (ctrl.i18n_key) {
                ctrl.content.clear();
                ctrl.add(this.t(ctrl.i18n_key, ctrl.i18n_params));
            }
        });
    }
}

// Usage
class WelcomeMessage extends I18nControl {
    constructor(spec) {
        super(spec);
        
        this.user_name = spec.user_name;
        this.i18n_key = 'welcome_message';
        this.i18n_params = { name: this.user_name };
        
        this.compose_message();
    }
    
    compose_message() {
        this.dom.tagName = 'h1';
        this.add(this.t(this.i18n_key, this.i18n_params));
    }
}

API Reference

Control Core Methods

Method Description
add(content) Add child control or text content
remove() Remove this control from parent
render() Generate HTML string
activate() Connect to DOM element (client-side)
style(property, value) Set CSS styles
add_class(name) Add CSS class
remove_class(name) Remove CSS class
has_class(name) Check if class exists
on(event, handler) Add event listener
raise(event, data) Emit custom event

Control Properties

Property Description
dom DOM-related properties and methods
content Collection of child controls
context Application context
data.model Business data model
view.data.model UI-specific view model
parent Parent control reference

Event Types

Event When Triggered
change Property or content changes
activate Control becomes active
click, mousedown, etc. DOM events
resize Size changes
move Position changes

Development Status

The framework is actively developed with focus on:

  • Enhanced data binding between models
  • Improved serialization for complex data structures
  • Standardized mixin patterns
  • Better debugging and development tools
  • Comprehensive testing coverage

For detailed implementation plans, see MVVM.md.

Installation

Prerequisites

  • Node.js 14+
  • npm or yarn package manager

Basic Installation

npm install jsgui3-html
# or
yarn add jsgui3-html

Dependencies

The framework depends on several core packages:

  • lang-tools - Core utilities and Evented_Class foundation
  • obext - Object extension utilities for properties and fields
  • fnl - Functional programming utilities (promises/callbacks)
  • jsgui3-gfx-core - Graphics and geometry utilities (Rect class)

Quick Start

Basic Control Creation

const jsgui = require('jsgui3-html');
const { Control } = jsgui;

// Create a simple button
const button = new Control({
    tagName: 'button',
    text: 'Hello World',
    class: 'btn primary'
});

// Server-side: Generate HTML
console.log(button.render());
// Output: <button class="btn primary" data-jsgui-id="ctrl_1">Hello World</button>

Building Your First Application

// app.js
const jsgui = require('jsgui3-html');
const { Control, Page_Context } = jsgui;

// Create application context
const context = new Page_Context();

// Create main application control
class App extends Control {
    constructor(spec) {
        super(spec);
        this.compose_app();
    }
    
    compose_app() {
        this.add(this.header = new Header({ context: this.context }));
        this.add(this.main = new MainContent({ context: this.context }));
        this.add(this.footer = new Footer({ context: this.context }));
    }
}

// Initialize app
const app = new App({ context });

Server-Side Rendering

// server.js
const express = require('express');
const app = express();

app.get('/', (req, res) => {
    const context = new Page_Context();
    const page = new MyPage({ context });
    
    const html = `
        <!DOCTYPE html>
        <html>
        <head>
            <title>My App</title>
            <link rel="stylesheet" href="/styles.css">
        </head>
        <body>
            ${page.render()}
            <script src="/client.js"></script>
        </body>
        </html>
    `;
    
    res.send(html);
});

Client-Side Activation

// client.js
const jsgui = require('jsgui3-html');

// Activate controls on page load
document.addEventListener('DOMContentLoaded', () => {
    const context = new jsgui.Page_Context();
    context.activate_page();
});

Component Library

Built-in Controls

Basic Controls

// Text display
const text = new Control({
    tagName: 'span',
    text: 'Hello World'
});

// Input field
const input = new Control({
    tagName: 'input',
    attrs: {
        type: 'text',
        placeholder: 'Enter text...'
    }
});

// Container
const container = new Control({
    tagName: 'div',
    class: 'container'
});
container.add(text);
container.add(input);

Layout Controls

// Grid layout
const Grid = require('./controls/organised/0-core/0-basic/grid');
const grid = new Grid({
    grid_size: [3, 3], // 3x3 grid
    size: [300, 300]
});

// Panel container
const Panel = require('./controls/organised/1-standard/6-layout/panel');
const panel = new Panel({
    title: 'My Panel',
    collapsible: true
});

// Tabbed interface  
const Tabbed_Panel = require('./controls/organised/1-standard/6-layout/tabbed-panel');
const tabs = new Tabbed_Panel({
    tabs: ['Tab 1', 'Tab 2', 'Tab 3']
});

Form Controls

// Checkbox
const checkbox = new Control({
    tagName: 'input',
    attrs: { type: 'checkbox' }
});

// Radio button group
const Radio_Button_Group = require('./controls/organised/0-core/0-basic/1-compositional/radio-button-group');
const radioGroup = new Radio_Button_Group({
    options: ['Option 1', 'Option 2', 'Option 3'],
    name: 'choices'
});

// Date picker (with view model formatting)
class DatePicker extends Data_Model_View_Model_Control {
    constructor(spec) {
        super(spec);
        this.data.model = new Data_Object({ date: new Date() });
        this.view.data.model = new Data_Object({ 
            formatted_date: this.format_date(this.data.model.date)
        });
    }
}

Custom Control Development

Simple Custom Control

class Counter extends Control {
    constructor(spec) {
        super(spec);
        
        this.count = spec.count || 0;
        this.compose_counter();
        this.setup_events();
    }
    
    compose_counter() {
        this.display = new Control({
            tagName: 'span',
            text: this.count.toString(),
            class: 'counter-display'
        });
        
        this.increment_btn = new Control({
            tagName: 'button',
            text: '+',
            class: 'counter-btn'
        });
        
        this.decrement_btn = new Control({
            tagName: 'button',
            text: '-',
            class: 'counter-btn'
        });
        
        this.add(this.decrement_btn);
        this.add(this.display);
        this.add(this.increment_btn);
    }
    
    setup_events() {
        this.increment_btn.on('click', () => {
            this.count++;
            this.display.content.clear();
            this.display.add(this.count.toString());
        });
        
        this.decrement_btn.on('click', () => {
            this.count--;
            this.display.content.clear();
            this.display.add(this.count.toString());
        });
    }
}

Advanced Control with Data Models

class UserProfile extends Data_Model_View_Model_Control {
    constructor(spec) {
        super(spec);
        
        // Data model - raw user data
        this.data.model = new Data_Object({
            id: spec.user_id,
            name: spec.name,
            email: spec.email,
            avatar_url: spec.avatar_url,
            created_at: new Date(spec.created_at)
        });
        
        // View model - formatted for display
        this.view.data.model = new Data_Object({
            display_name: this.data.model.name,
            display_email: this.data.model.email,
            avatar_src: this.data.model.avatar_url || '/default-avatar.png',
            member_since: this.format_date(this.data.model.created_at),
            is_editing: false
        });
        
        this.setup_bindings();
        this.compose_profile();
    }
    
    setup_bindings() {
        // Auto-sync data to view model
        this.data.model.on('change', e => {
            switch(e.name) {
                case 'name':
                    this.view.data.model.display_name = e.value;
                    break;
                case 'email':
                    this.view.data.model.display_email = e.value;
                    break;
            }
        });
        
        // Update UI when view model changes
        this.view.data.model.on('change', e => {
            if (e.name === 'is_editing') {
                this.toggle_edit_mode(e.value);
            }
        });
    }
    
    compose_profile() {
        this.avatar = new Control({
            tagName: 'img',
            attrs: { src: this.view.data.model.avatar_src },
            class: 'user-avatar'
        });
        
        this.name_display = new Control({
            tagName: 'h2',
            text: this.view.data.model.display_name,
            class: 'user-name'
        });
        
        this.edit_btn = new Control({
            tagName: 'button',
            text: 'Edit',
            class: 'edit-btn'
        });
        
        this.add(this.avatar);
        this.add(this.name_display);
        this.add(this.edit_btn);
        
        this.edit_btn.on('click', () => {
            this.view.data.model.is_editing = !this.view.data.model.is_editing;
        });
    }
}

Performance Considerations

Rendering Optimization

The framework uses direct DOM manipulation instead of virtual DOM:

// Efficient - updates only what changed
this.data.model.on('change', e => {
    if (e.name === 'title') {
        this.title_element.content.clear();
        this.title_element.add(e.value);
    }
});

// Avoid - unnecessary full re-render
this.data.model.on('change', e => {
    this.clear();
    this.compose(); // Rebuilds entire control
});

Memory Management

// Clean up event listeners when control is removed
remove() {
    this.data.model.off('change', this.data_change_handler);
    this.view.data.model.off('change', this.view_change_handler);
    super.remove();
}

Large Data Sets

// Use pagination for large lists
class DataList extends Control {
    constructor(spec) {
        super(spec);
        this.page_size = spec.page_size || 50;
        this.current_page = 0;
        this.render_page();
    }
    
    render_page() {
        const start = this.current_page * this.page_size;
        const end = start + this.page_size;
        const page_data = this.data.slice(start, end);
        
        this.clear();
        page_data.forEach(item => {
            this.add(new ListItem({ data: item }));
        });
    }
}

Testing

Unit Testing Controls

// test/controls/button.test.js
const { Control } = require('jsgui3-html');

describe('Button Control', () => {
    let button;
    
    beforeEach(() => {
        button = new Control({
            tagName: 'button',
            text: 'Test Button'
        });
    });
    
    test('renders correct HTML', () => {
        const html = button.render();
        expect(html).toContain('<button');
        expect(html).toContain('Test Button');
        expect(html).toContain('data-jsgui-id');
    });
    
    test('handles click events', () => {
        let clicked = false;
        button.on('click', () => { clicked = true; });
        
        // Simulate activation and click
        button.activate();
        button.raise('click');
        
        expect(clicked).toBe(true);
    });
});

Integration Testing

// test/integration/form.test.js
const { JSDOM } = require('jsdom');

describe('Form Integration', () => {
    let dom, document, window;
    
    beforeEach(() => {
        dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
        document = dom.window.document;
        window = dom.window;
        global.document = document;
        global.window = window;
    });
    
    test('form submission updates data model', () => {
        const form = new ContactForm({
            context: new Page_Context()
        });
        
        // Render and activate
        document.body.innerHTML = form.render();
        form.activate();
        
        // Simulate user input
        const nameInput = document.querySelector('input[name="name"]');
        nameInput.value = 'John Doe';
        nameInput.dispatchEvent(new window.Event('input'));
        
        expect(form.data.model.name).toBe('John Doe');
    });
});

Debugging

Development Tools

// Enable debug mode for detailed logging
ctrl.debug_mode = true;

// Inspect control state
console.log(ctrl.inspect()); // Shows all properties and state

// Trace event flow
ctrl.on('*', (event_name, event_data) => {
    console.log(`Event: ${event_name}`, event_data);
});

Common Issues and Solutions

Controls Not Activating

// Problem: Controls don't respond to events
// Solution: Ensure proper activation
const context = new Page_Context();
context.activate_page(); // Activates all controls on page

State Not Persisting

// Problem: State lost on page reload
// Solution: Implement serialization
ctrl.on('server-pre-render', () => {
    ctrl._fields = ctrl._fields || {};
    ctrl._fields.important_state = ctrl.important_value;
});

Memory Leaks

// Problem: Event listeners not cleaned up
// Solution: Proper cleanup in remove()
remove() {
    // Remove all event listeners
    this.off(); // Removes all listeners on this control
    
    // Clean up child controls
    this.content.each(child => {
        if (child.remove) child.remove();
    });
    
    super.remove();
}

Browser Compatibility

Supported Browsers

  • Chrome 70+
  • Firefox 65+
  • Safari 12+
  • Edge 79+

Polyfills Required

For older browsers, include polyfills for:

  • WeakMap and WeakSet
  • Object.assign
  • Array.prototype.find
  • Promise (if using async features)

Feature Detection

// Check for required features
if (typeof WeakMap === 'undefined') {
    console.error('WeakMap not supported - please include polyfill');
}

// Graceful degradation
if (!window.addEventListener) {
    // Fallback for very old browsers
    ctrl.add_event_listener = function(event, handler) {
        this.dom.el.attachEvent('on' + event, handler);
    };
}

Security Considerations

XSS Prevention

// Safe text rendering (automatically escaped)
const safe_text = new Control({
    tagName: 'div',
    text: user_input // Automatically escaped
});

// Raw HTML (use with caution)
const raw_html = new Control({
    tagName: 'div'
});
raw_html.dom.el.innerHTML = sanitized_html; // Only use with sanitized content

Data Validation

class SecureForm extends Control {
    validate_input(value, type) {
        switch(type) {
            case 'email':
                return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
            case 'phone':
                return /^\d{10}$/.test(value.replace(/\D/g, ''));
            default:
                return value.length > 0;
        }
    }
    
    sanitize_input(value) {
        return value.replace(/[<>]/g, ''); // Basic sanitization
    }
}

Contributing

Development Setup

# Clone repository
git clone https://github.com/jsgui3/jsgui3-html.git
cd jsgui3-html

# Install dependencies
npm install

# Run tests
npm test

# Run linter
npm run lint

Code Style Guidelines

  • Use camelCase for JavaScript variables and methods
  • Use PascalCase for class names
  • Use snake_case for file names
  • Include JSDoc comments for public methods
  • Follow the existing patterns for mixins and controls

Submitting Changes

  1. Fork the repository
  2. Create a feature branch: git checkout -b my-feature
  3. Make your changes with tests
  4. Run the test suite: npm test
  5. Submit a pull request with detailed description

Reporting Issues

When reporting bugs, please include:

  • Minimal code example reproducing the issue
  • Expected vs actual behavior
  • Browser and Node.js versions
  • Stack trace if available

Roadmap

Version 2.x (Current)

  • βœ… Core control system with isomorphic rendering
  • βœ… Basic mixins (selectable, dragable, press events)
  • βœ… Event system and DOM management
  • πŸ”„ Enhanced data binding system
  • πŸ”„ Improved serialization for complex data

Version 3.x (Planned)

  • πŸ“‹ Standardized mixin state management
  • πŸ“‹ Advanced validation framework
  • πŸ“‹ Performance monitoring tools
  • πŸ“‹ Component hot-reloading
  • πŸ“‹ TypeScript definitions

Version 4.x (Future)

  • πŸ“‹ WebComponent integration
  • πŸ“‹ Advanced animation system
  • πŸ“‹ Mobile-optimized controls
  • πŸ“‹ Accessibility enhancements

License

This project is licensed under the MIT License - see the LICENSE file for details.

Support

File Structure and Organization

The jsgui3-html framework follows a structured organization:

jsgui3-html/
β”œβ”€β”€ html-core/                    # Core framework files
β”‚   β”œβ”€β”€ control-core.js          # Base Control_Core class
β”‚   β”œβ”€β”€ control-enh.js           # Enhanced Control class  
β”‚   β”œβ”€β”€ control.js               # Main Control export
β”‚   β”œβ”€β”€ Control_View.js          # View management
β”‚   β”œβ”€β”€ Control_View_UI.js       # UI-specific view logic
β”‚   └── Data_Model_View_Model_Control.js  # MVVM control base
β”œβ”€β”€ control_mixins/               # Reusable behavior mixins
β”‚   β”œβ”€β”€ selectable.js            # Selection functionality
β”‚   β”œβ”€β”€ dragable.js              # Drag and drop
β”‚   β”œβ”€β”€ press-events.js          # Touch/press event handling
β”‚   └── pressed-state.js         # Visual press feedback
└── controls/organised/           # Pre-built control library
    β”œβ”€β”€ 0-core/0-basic/          # Core controls (Grid, List)
    └── 1-standard/6-layout/     # Layout controls (Panel, Tabbed_Panel)

Framework Philosophy

Jsgui3-html is built on several key principles:

  1. Isomorphic First: Every component works identically on server and client
  2. Direct DOM Manipulation: No virtual DOM - direct, predictable updates
  3. Compositional Architecture: Build complex UIs from simple, reusable pieces
  4. State Separation: Clear distinction between business data and UI state
  5. Event-Driven: Reactive updates through comprehensive event system
  6. Mixin-Based Extensions: Add functionality without complex inheritance

Key Differences from Other Frameworks

Feature jsgui3-html React Vue Angular
Virtual DOM ❌ Direct DOM βœ… βœ… ❌ Direct DOM
Server Rendering βœ… Built-in βœ… Next.js βœ… Nuxt.js βœ… Universal
State Management Data/View Models External (Redux) Vuex/Pinia Services/NgRx
Component Model Class-based Function/Class Object/Composition Class-based
Learning Curve Medium High Low High
Bundle Size Small Medium Small Large

Troubleshooting

Common Installation Issues

Node.js Version Compatibility

# Check Node.js version
node --version  # Should be 14.0.0 or higher

# If using nvm, switch to compatible version
nvm install 16
nvm use 16

Package Installation Problems

# Clear npm cache if installation fails
npm cache clean --force

# Delete node_modules and package-lock.json, then reinstall
rm -rf node_modules package-lock.json
npm install

# Use yarn if npm has issues
yarn install

Runtime Error Solutions

"Cannot find module 'lang-tools'" Error

# Install missing dependency
npm install lang-tools

# Or install all dependencies
npm install obext fnl jsgui3-gfx-core

Controls Not Rendering on Server

// Ensure proper context setup
const { Page_Context } = require('jsgui3-html');
const context = new Page_Context();

// Register all controls with context before rendering
app.register_control(my_control);

Client-Side Activation Failures

// Check for proper DOM ready handling
document.addEventListener('DOMContentLoaded', () => {
    // Only activate after DOM is fully loaded
    const context = new jsgui.Page_Context();
    context.activate_page();
});

// Verify script loading order
// 1. jsgui3-html library
// 2. Your application code
// 3. Activation code

Performance Issues

Slow Initial Page Load

// Use lazy loading for non-critical controls
class LazyControl extends Control {
    constructor(spec) {
        super(spec);
        this.lazy_loaded = false;
    }
    
    activate() {
        if (!this.lazy_loaded) {
            this.compose_heavy_content();
            this.lazy_loaded = true;
        }
        super.activate();
    }
}

Memory Usage Growing Over Time

// Implement proper cleanup in long-running applications
class ManagedControl extends Control {
    constructor(spec) {
        super(spec);
        this.cleanup_handlers = [];
    }
    
    add_managed_listener(target, event, handler) {
        target.on(event, handler);
        this.cleanup_handlers.push(() => target.off(event, handler));
    }
    
    destroy() {
        this.cleanup_handlers.forEach(cleanup => cleanup());
        this.cleanup_handlers = [];
        super.remove();
    }
}

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •