tiny-engine-core
v1.8.0
Published
Tiny tree-shakable core engine library
Maintainers
Readme
Tiny engine (Core)
A minimalist, framework-agnostic, and tree‑shakable JavaScript runtime for building interactive UI components—without dependencies.
This core edition gives you lightweight component lifecycle management (register, init, observe, etc.) and a simple foundation for your own plugins (accordion, modal, dropdown, etc.).
Features
- Framework‑independent: Works with plain HTML, React, Vue, or Svelte.
- Zero dependencies: Pure TypeScript → compiled using Esbuild.
- Tree‑shakable: Import only what you need.
- Component lifecycle helpers:
on(),destroy(),emit(), etc. - Automatic initialization: Discovers elements with configurable
ui-*attributes (customizable prefix). - Dynamic prefix support:
UI.config({ prefix })+getPrefix()utility. - TinyRequest: A tiny dependency-free request layer built on native fetch with timeout, retry, abort, caching, and interceptors.
- Lightweight: approx. 6KB gzipped.
- SSR-safe imports: ESM, CommonJS, and DataGrid exports can load without browser globals.
1.8.0 (2026-06-10)
Install Dependencies
npm
npm install tiny-engine-coreyarn
yarn add tiny-engine-corepnpm
pnpm add tiny-engine-coreIn Modern JS (ES Modules)
import { UI, Capsule, getPrefix } from 'tiny-engine-core';
class Tabs extends Capsule {
constructor(el, options = {}) {
super(el, options);
const prefix = getPrefix(); // 'ui' or configured prefix
this.onPropChange('active', (next) => {
console.log('Active tab:', next);
});
this.on(this.el, 'click', (event) => {
const action = event.target.closest('[data-tab]');
if (!action) return;
this.props.active = action.getAttribute('data-tab');
this.emit('tabs:change', { active: this.props.active });
UI.emit('tabs:global-change', { active: this.props.active });
});
console.log(`Tabs using ${prefix}- prefix`);
}
}
UI.config({ prefix: 'app' }); // Optional: change to 'app-'
UI.register('tabs', Tabs);
UI.init();
UI.observe();v1.8.0 DataGrid
import { UI, createDataGridPlugin } from 'tiny-engine-core';
import 'tiny-engine-core/data-grid/style.css';
UI.use(createDataGridPlugin());
UI.getOrCreate(document.querySelector('[ui-data-grid]'), 'data-grid', {
columns: [
{ id: 'name', header: 'Name' },
{ id: 'role', header: 'Role' }
],
rows: users,
rowKey: 'id',
searchable: true,
pagination: true,
pageSize: 10,
pageSizeOptions: [10, 25, 50],
selection: 'multiple'
});The header checkbox selects or clears the visible page. Sorting is enabled by
default and can be disabled per column with sortable: false.
You can also import the class directly:
import { DataGrid } from 'tiny-engine-core/data-grid';
import { DataGrid as RootDataGrid } from 'tiny-engine-core';Free DataGrid Foundation
const grid = UI.getOrCreate(document.querySelector('[ui-data-grid]'), 'data-grid', {
columns: [
{ field: 'name', title: 'Name' },
{ field: 'email', title: 'Email' },
{
field: 'status',
title: 'Status',
render: (value) => `<span class="badge">${value}</span>`
},
{
field: 'actions',
title: 'Actions',
sortable: false,
filterable: false,
render: (_, row) => `
<button data-action="edit" data-id="${row.id}">Edit</button>
<button data-action="delete" data-id="${row.id}">Delete</button>
`
}
],
rows: users,
rowKey: 'id',
searchable: true,
pagination: true,
stickyHeader: true,
persist: true,
persistKey: 'users-grid',
urlSync: true,
emptyState: 'No records found'
});
grid.hideColumn('email');
grid.moveColumn(0, 1);
grid.loading(true);
grid.loading(false);
grid.exportCSV('users.csv');
const offAction = grid.on('action', (detail) => {
console.log(detail.action, detail.row);
});
const offVisibility = grid.on('column:visibility', (detail) => {
console.log(detail.visibleColumns);
});Supported free foundation features include:
- Column visibility with
showColumn(),hideColumn(),toggleColumn(), andgetVisibleColumns() - Column ordering with
moveColumn(),setColumnOrder(), andgetColumnOrder() - Action-cell event delegation through
grid.on('action', handler) - Loading rows, sticky header mode, custom empty state text, and HTML renderers
- CSV export for the current filtered and sorted dataset
- Optional browser-only persistence and URL query sync without breaking SSR imports
v1.7.0 TinyRequest
import { request, TinyRequest } from 'tiny-engine-core';
const users = await request.get('/api/users', {
cache: true,
timeout: 5000,
retry: 2
});
await request.post('/api/users', {
name: 'Urvesh'
});
request.interceptors.request.use((url, init) => ({
...init,
headers: {
...Object.fromEntries(new Headers(init.headers)),
authorization: 'Bearer token'
}
}));
request.timeout(5000).retry(2).cache(true);
request.abort('/api/users');
const api = new TinyRequest({ baseUrl: '/api', timeout: 5000 });v1.6.0 Production Runtime APIs
import { UI } from 'tiny-engine-core';
UI.config({
prefix: 'app',
hydrate: true // Trust SSR markup and avoid no-op option writes.
});
UI.register('modal', Modal);
// Full document boot.
UI.init();
UI.observe();
// Partial scan for portals, AJAX blocks, route fragments, or hydration islands.
UI.scan(document.querySelector('#route-fragment'));
// Explicit teardown for React/Vue unmounts, route changes, HMR, or micro-frontends.
UI.destroy(document.querySelector('#route-fragment'));
// Call with no root to destroy all active capsules and stop global observers.
UI.destroy();
console.log(UI.devtools().inspect().metrics);Prefix System
// Default: ui-*
UI.register('tabs', Tabs); // Finds [ui-tabs]
// Custom prefix
UI.config({ prefix: 'app' }); // Finds [app-tabs]
UI.register('tabs', Tabs);
// In components
import { getPrefix } from 'tiny-engine-core';
class MyComponent extends Capsule {
constructor(el, options = {}) {
super(el, options);
const prefix = getPrefix(); // 'app' (syncs with UI.config)
}
}Capsule Base Class
class MyComponent extends Capsule {
constructor(el, options = {}) {
super(el, options);
this.on(this.el, 'click', this.handleClick); // Auto-cleanup
this.onPropChange('open', (next, prev) => {
console.log('open changed:', prev, '->', next);
});
}
handleClick = () => {
const event = this.emit('before:open', { id: 1 }, { cancelable: true });
if (event.defaultPrevented) return;
this.props.open = true;
UI.emit('modal:opened', { id: this.uid });
};
destroy() {
super.destroy(); // Removes all listeners and store subscriptions
}
}HTML + Custom Prefix
<!-- app- prefix (configured via UI.config) -->
<div id="settingsModal" app-modal app-modal-open="false" ref="modal">
<button
type="button"
data-app-toggle="modal"
data-app-action="close"
data-target="#settingsModal"
>
Close
</button>
</div>
<button
type="button"
data-app-toggle="modal"
data-target="#settingsModal"
>
Open
</button>React Functional Component
import { useEffect, useRef } from 'react';
import { UI } from 'tiny-engine-core';
UI.config({ prefix: 'app' });
UI.register('modal', (el, api) => {
const setState = (open) => {
api.props.open = open;
el.setAttribute('data-state', open ? 'open' : 'closed');
UI.emit('modal:change', { open, id: api.uid });
};
return {
open() {
setState(true);
},
close() {
const event = api.emit('before:close', null, { cancelable: true });
if (event.defaultPrevented) return false;
setState(false);
return true;
},
toggle() {
if (api.props.open) return this.close();
this.open();
return true;
}
};
});
export function ModalDemo() {
const hostRef = useRef(null);
useEffect(() => {
if (!hostRef.current) return;
const instance = UI.getOrCreate(hostRef.current, 'modal', { open: false });
const off = UI.on('modal:change', (event) => {
console.log('Modal changed:', event.detail);
});
return () => {
off();
instance?.destroy();
};
}, []);
return (
<>
<div ref={hostRef} app-modal app-modal-open="false">
React-powered modal host
</div>
<button
type="button"
data-app-toggle="modal"
data-target="[app-modal]"
>
Toggle modal
</button>
</>
);
}Functional Capsule
import { UI } from 'tiny-engine-core';
UI.register('modal', (el, api) => {
const setState = (open) => {
api.props.open = open;
el.setAttribute('data-state', open ? 'open' : 'closed');
UI.emit('modal:change', { open, id: api.uid });
};
api.onPropChange('open', (open) => {
el.setAttribute('aria-hidden', String(!open));
});
return {
open() {
setState(true);
},
close() {
const event = api.emit('before:close', null, { cancelable: true });
if (event.defaultPrevented) return false;
setState(false);
return true;
},
toggle() {
if (api.props.open) return this.close();
this.open();
return true;
},
syncOptions(nextOptions) {
el.setAttribute('data-state', nextOptions.open ? 'open' : 'closed');
},
destroy() {
el.removeAttribute('data-state');
}
};
});Store Middleware
import { CapsuleStore } from 'tiny-engine-core';
const store = new CapsuleStore(
(state, action) => {
if (action.type === 'increment') {
return { ...state, count: state.count + 1 };
}
return state;
},
{ count: 0 }
);
store.use((action, state) => {
console.log('before action:', action.type, state.count);
if (action.type === 'increment' && state.count >= 10) {
return false; // cancel the action
}
return action;
});
store.connect((state, action) => {
console.log('after action:', action.type, state.count);
});Complete API Reference
| Feature | TypeScript | JavaScript | HTML Example |
| ---------------------- | ------------------------------------------------ | ---------- | -------------------------------- |
| Config | UI.config({ prefix: 'app' }) | Same | |
| Hydration | UI.config({ hydrate: true }) | Same | SSR-safe resume mode |
| Register | UI.register('tabs', Tabs) | Same | (default) |
| Prefix Utility | getPrefix() | Same | Dynamic selectors |
| Init | UI.init() | Same | Auto-finds [prefix-name] |
| Partial scan | UI.scan(root) | Same | Hydration islands / portals |
| Observe | UI.observe() | Same | Dynamic content |
| Refs | this.refs.toggle | Same | ref="toggle" |
| Directives | Auto-bound | Auto-bound | @click="select('Home')" |
| Events | this.emit('change', data) | Same | @change="onChange" |
| Lifecycle | on/offAll/destroy | Same | Event management |
| Auto-init refresh | UI.init(root) | Same | Nested [ui-tabs] mounts |
| Attribute observation | UI.observe() | Same | ui-tabs-active="2" |
| Dynamic directives | Delegated @click / @change | Same | @click="next()" |
| Root + lazy refs | this.refs.panel | Same | ref="panel" |
| Option sync | instance.syncOptions(nextOptions) | Same | Live ui-modal-open="true" |
| Safe DOM cleanup | UI.destroy(root?) | Same | Explicit cleanup on unmount |
| Host instance expose | el.tabs, el.modal, el.dropdown | Same | Direct host element access |
| Store middleware | store.use((action, state) => action) | Same | Action pipeline |
| Cancelable events | this.emit('close', data, { cancelable: true }) | Same | event.preventDefault() support |
| Data API triggers | UI.init() / UI.observe() | Same | data-ui-toggle="modal" |
| Data target lookup | Auto target resolution | Same | data-target="#settingsModal" |
| Global UI bus | UI.on('modal:open', fn) / UI.emit(...) | Same | Cross-component communication |
| Functional capsules | UI.register('modal', (el, api) => ({ ... })) | Same | Function-based lifecycle |
| DX upgrade | UI.config({ debug: true, warnings: true }) | Same | Safer developer workflow |
| Devtools | UI.devtools() / window.__TINY_ENGINE__ | Same | Inspect runtime internals |
| Performance metrics | UI.devtools().inspect().metrics | Same | Creates, scans, flush timings |
| Plugin system | UI.use(plugin) | Same | Third-party engine extensions |
| Tiny request api | { request, TinyRequest } | Same | Faster runtime hot paths |
License
Released under the MIT License. © 2025 Tiny engine Authors — open‑source forever.
