
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
jsgui3-html
Advanced tools
Jsgui HTML generation and processing module (isomorphic, runs on client and server, few npm requirements)
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.
Controls are the fundamental building blocks of jsgui3-html applications. They are analogous to React Components but designed with a focus on:
Built on the Evented_Class from the lang-tools package, controls can:
Controls use a compositional model where:
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: 'user@example.com',
created_at: new Date()
});
View Model: Contains UI-specific representations
this.view.data.model = new Data_Object({
formatted_email: 'user@example.com',
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 are composable functions that enhance controls with reusable behavior. Each mixin is a function (ctrl, options?) => void|cleanup that adds properties, events, and DOM behaviors to any control. The framework ships with 39 mixins organized into 7 categories.
📘 Detailed docs: control_mixins/README.md for the full catalog and API tables, or docs/mixins-book.md for the comprehensive deep-dive reference.
const selectable = require('./control_mixins/selectable');
const collapsible = require('./control_mixins/collapsible');
class TreeNode extends Control {
constructor(spec) {
super(spec);
selectable(this, null, { multi: true });
collapsible(this, { trigger: '.header', content: '.children' });
}
}
| Mixin | Purpose |
|---|---|
press-events | Unified mouse/touch press handling with timing, drag detection, and hold |
pressed-state | Visual pressed CSS class feedback on press (disposable) |
dragable | Full drag-and-drop with axis locking and bounds |
selectable | Click-to-select with multi-select (Shift/Ctrl) |
selection-box-host | Marquee/lasso drag-selection |
resizable | Element resizing via drag handles |
keyboard_navigation | Arrow key navigation with roving tabindex (ARIA) |
collapsible | Expand/collapse with aria-expanded and CSS classes |
press-outside | Click-away detection |
fast-touch-click | Eliminates 300ms touch delay |
| Mixin | Purpose |
|---|---|
input_base | Core get_value() / set_value() / focus() / blur() |
input_validation | Pluggable validators, async support, built-in email/number/range |
input_mask | Real-time formatting for phone, date, currency |
input_api | High-level wiring: base + validation + mask |
field_status | Dirty/pristine/touched state tracking |
display-modes, display, popup, bind, coverable, virtual_window, collapsible — size modes, popup positioning, spatial binding, virtual scrolling, expand/collapse.
theme, themeable, theme_params — CSS variable tokens, size/variant parameters, theme resolution.
activation, hydration, swap_registry, auto_enhance, mx — progressive enhancement, SSR hydration, mutation-observer auto-activation, mixin directory.
mixin_cleanup, mixin_registry — disposable mixin support, formal dependency/conflict metadata.
a11y, link-hovers, deletable, selected-deletable, selected-resizable — ARIA helpers, delete/resize on selected items.
const { create_mixin_cleanup } = require('./control_mixins/mixin_cleanup');
const my_mixin = (ctrl, options = {}) => {
// 1. Guard against double-apply
ctrl.__mx = ctrl.__mx || {};
if (ctrl.__mx.my_mixin) return ctrl.__mx.my_mixin;
// 2. Create disposable cleanup handle
const cleanup = create_mixin_cleanup(ctrl, 'my_mixin');
ctrl.__mx.my_mixin = cleanup;
// 3. Add behavior
const handler = (e) => { /* ... */ };
ctrl.on('click', handler);
cleanup.track_listener(ctrl, 'click', handler);
return cleanup; // caller can later call cleanup.dispose()
};
const mx = require('./control_mixins/mx');
mx.has_feature(ctrl, 'selectable'); // true/false
mx.list_features(ctrl); // ['selectable', 'press_events', ...]
Mixins auto-resolve dependencies — you never need to worry about application order:
// pressed-state automatically applies press-events if missing
// selectable automatically applies press-events if missing
// selection-box-host automatically applies selectable + press-events
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:
For detailed implementation plans, see MVVM.md.
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);
});
This activation step is often called "Hydration" in other UI frameworks.
// client.js
const jsgui = require('jsgui3-html');
// Activate controls on page load
document.addEventListener('DOMContentLoaded', () => {
const context = new jsgui.Page_Context();
context.activate_page();
});
Controls are grouped by stability tier:
controls/controls.js.controls.experimental.controls.deprecated (emit warnings).// 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 }));
});
}
}
Run all Playwright E2E suites from the repository root:
npm run test:playwright:all
This delegates to test/e2e and runs the aggregate Playwright runner.
// 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();
}
For older browsers, include polyfills for:
WeakMap and WeakSetObject.assignArray.prototype.findPromise (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
}
}
For detailed information and quick starts:
The dev-examples directory now includes three comprehensive examples:
Enhanced Counter (dev-examples/binding/counter/) - ⭐ ENHANCED
User Form (dev-examples/binding/user-form/)
WYSIWYG Form Builder (dev-examples/wysiwyg-form-builder/) - ⭐ NEW (WIP)
Three new controls added to the framework:
FormField - Composite control combining label + input + validation indicatorToolbar - Flexible button container with icons, tooltips, separatorsPropertyEditor - Dynamic property editing panel that adapts to item typeAll available via: const { FormField, Toolbar, PropertyEditor } = require('jsgui3-html');
# Clone repository
git clone https://github.com/jsgui3/jsgui3-html.git
cd jsgui3-html
# Install dependencies
npm install
# Install test dependencies
cd test && npm install
# Run all tests
npm test
# Run specific test suites
npm run test:core # Core control tests
npm run test:mvvm # MVVM and binding tests
npm run test:integration # Integration tests
# Run linter
npm run lint
git checkout -b my-featurenpm testFor responsive/multi-device Tier 1 control upgrades, use:
docs/books/adaptive-control-improvements/README.md.github/pull_request_template_adaptive_tier1.mdWhen reporting bugs, please include:
For detailed control improvement planning and checklists, see:
docs/jsgui3_html_improvement_plan.mddocs/jsgui3_html_improvement_priorities.mddocs/improvement_checklists/INDEX.mdThis project is licensed under the MIT License - see the LICENSE file for details.
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:
| 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();
}
}
FAQs
Jsgui HTML generation and processing module (isomorphic, runs on client and server, few npm requirements)
The npm package jsgui3-html receives a total of 20 weekly downloads. As such, jsgui3-html popularity was classified as not popular.
We found that jsgui3-html demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.