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.
- Core Concepts
- Architecture Overview
- Control Lifecycle
- State Management
- Mixins System
- Rendering and DOM Management
- Event System
- Examples
- API Reference
- Installation
- Quick Start
- Component Library
- Testing
- Performance Considerations
- Debugging
- Browser Compatibility
- Security Considerations
- Contributing
- Roadmap
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
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
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
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
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 |
const button = new Control({
tagName: 'button',
text: 'Click me',
class: 'primary-btn'
});
Controls build their internal structure:
compose() {
this.add(new Icon({ name: 'check' }));
this.add(new Text({ value: this.label }));
}
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>
Connect rendered HTML to control instances:
control.activate(); // Binds to existing DOM element
Handle user interactions and data changes:
control.on('click', () => {
this.data.model.count++;
});
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
});
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 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">
// 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));
}
}
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;
}
}
}
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 provide reusable functionality that can be applied to any control:
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 |
const mx_selectable = require('./control_mixins/selectable');
class ListItem extends Control {
constructor(spec) {
super(spec);
mx_selectable(this); // Adds selection capability
}
}
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;
});
};
Controls generate HTML strings for server-side rendering:
renderBeginTagToHtml() {
return `<${this.dom.tagName}${this.renderDomAttributes()}>`;
}
renderEndTagToHtml() {
return `</${this.dom.tagName}>`;
}
Attributes are managed reactively:
this.dom.attrs.class = 'active selected';
this.dom.attrs.style.color = 'red';
// Automatically updates DOM when activated
// 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' });
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;
});
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);
});
Events bubble up through the control hierarchy:
parent_control.on('child_selected', e => {
console.log('Child control selected:', e.ctrl_target);
});
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);
}
});
}
}
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 }));
}
}
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);
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;
}
}
// 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);
}
}
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);
}
}
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));
}
}
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 |
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 | When Triggered |
---|---|
change |
Property or content changes |
activate |
Control becomes active |
click , mousedown , etc. |
DOM events |
resize |
Size changes |
move |
Position changes |
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.
- Node.js 14+
- npm or yarn package manager
npm install jsgui3-html
# or
yarn add jsgui3-html
The framework depends on several core packages:
lang-tools
- Core utilities and Evented_Class foundationobext
- Object extension utilities for properties and fieldsfnl
- Functional programming utilities (promises/callbacks)jsgui3-gfx-core
- Graphics and geometry utilities (Rect class)
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>
// 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.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.js
const jsgui = require('jsgui3-html');
// Activate controls on page load
document.addEventListener('DOMContentLoaded', () => {
const context = new jsgui.Page_Context();
context.activate_page();
});
// 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);
// 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']
});
// 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)
});
}
}
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());
});
}
}
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;
});
}
}
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
});
// 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();
}
// 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 }));
});
}
}
// 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);
});
});
// 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');
});
});
// 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);
});
// Problem: Controls don't respond to events
// Solution: Ensure proper activation
const context = new Page_Context();
context.activate_page(); // Activates all controls on page
// 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;
});
// 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();
}
- Chrome 70+
- Firefox 65+
- Safari 12+
- Edge 79+
For older browsers, include polyfills for:
WeakMap
andWeakSet
Object.assign
Array.prototype.find
Promise
(if using async features)
// 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);
};
}
// 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
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
}
}
# 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
- 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
- Fork the repository
- Create a feature branch:
git checkout -b my-feature
- Make your changes with tests
- Run the test suite:
npm test
- Submit a pull request with detailed description
When reporting bugs, please include:
- Minimal code example reproducing the issue
- Expected vs actual behavior
- Browser and Node.js versions
- Stack trace if available
- β 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
- π Standardized mixin state management
- π Advanced validation framework
- π Performance monitoring tools
- π Component hot-reloading
- π TypeScript definitions
- π WebComponent integration
- π Advanced animation system
- π Mobile-optimized controls
- π Accessibility enhancements
This project is licensed under the MIT License - see the LICENSE file for details.
- π Documentation
- π¬ Community Forum
- π Issue Tracker
- π§ Email Support
- π‘ Feature Requests
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)
Jsgui3-html is built on several key principles:
- Isomorphic First: Every component works identically on server and client
- Direct DOM Manipulation: No virtual DOM - direct, predictable updates
- Compositional Architecture: Build complex UIs from simple, reusable pieces
- State Separation: Clear distinction between business data and UI state
- Event-Driven: Reactive updates through comprehensive event system
- Mixin-Based Extensions: Add functionality without complex inheritance
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 |
# 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
# 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
# Install missing dependency
npm install lang-tools
# Or install all dependencies
npm install obext fnl jsgui3-gfx-core
// 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);
// 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
// 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();
}
}
// 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();
}
}