simpl4u
v1.0.3
Published
A modular framework for building Electron and web applications using native Web Components, Bootstrap 5 and a reactive state management system.
Maintainers
Readme
Simpl4u
Simpl4u is a modular framework for building Electron and web applications using native Web Components, Bootstrap 5, and a reactive state management system. It provides a library of reusable components, services, and base classes to streamline the creation of dynamic, interactive user interfaces — with no build step required.
WIP — Work in Progress
POC — Proof of Concept
Features
- Custom Web Components — 18 reusable components: tables, forms, buttons, inputs, navbars, modals, color/date pickers, file uploads, progress bars, toggles, combo-boxes, code editor (Ace), todo/kanban boards, and more.
- Reactive State Management — Proxy-based global state store (
SimplModel) with context namespacing, deep reactivity, and subscriber notification. - Localization — Built-in multi-language support with 5 locales (English, Catalan, Spanish, German, Japanese) and string interpolation.
- Theme Management — Dynamic light/dark theme switching with system preference detection.
- CRUD Operations — Full create, read, update, delete flows via
SimplCrudandSimplTable. - Hash-based Routing — Lightweight single-page application routing via
RouterService. - Modal Dialogs — Bootstrap-powered message, confirm, and prompt dialogs via
ModalService. - Toast Notifications — Success, error, warning, and info notifications via
ToastService(powered by Notyf). - Storage Abstraction — Three-tier persistence (IndexedDB, sessionStorage, Electron IPC) via
StorageService/StorageAdapter. - File Operations — Download files in the browser, plus full file system access in Electron (read, write, copy, delete, directory selection).
- Bootstrap 5 — All components render Bootstrap-compatible markup.
- No Build Step — Pure ES modules loaded directly in modern browsers or Electron renderers.
- Component Scaffolder —
s4uBash CLI generates and auto-registers new components.
Architecture
HTMLElement
└── Element # Base class: rendering, events, validation, view state
├── StaticElement # State-aware, manual re-render
│ └── FormElement # Form input base (required, disabled, reactive, etc.)
│ ├── simpl-ace-editor
│ ├── simpl-button
│ ├── simpl-checkboxes
│ ├── simpl-color
│ ├── simpl-combobox
│ ├── simpl-combobox-list
│ ├── simpl-date
│ ├── simpl-file
│ ├── simpl-input
│ ├── simpl-progress
│ ├── simpl-select
│ ├── simpl-switch
│ └── simpl-textarea
│
└── ReactiveElement # Auto re-renders on state changes
├── simpl-navbar
├── simpl-table
└── simpl-todo
StaticElement (direct)
├── simpl-crud
└── simpl-spinnerCore (/core)
| Class | Description |
|---|---|
| Element | Root base extending HTMLElement. Provides templating (template(), render()), attribute-based event binding ((click)="method"), model integration, lifecycle-managed message-bus messaging (on() / emit()), Bootstrap form validation, view state persistence, and CSS scoping. |
| StaticElement | Extends Element. Subscribes to SimplModel changes and calls onUpdateState(property) — does not auto-re-render. |
| FormElement | Extends StaticElement. Base for form input components with required, disabled, hidden, reactive, type, placeholder attributes. |
| ReactiveElement | Extends Element. Subscribes to SimplModel and automatically re-renders the template on state changes. Uses morphdom-based in-place DOM patching to avoid unnecessary DOM replacement and flicker. |
Project Structure
simpl4u/
├── adapters/ # Storage adapters (e.g., StorageAdapter)
├── assets/
│ └── i18n/ # Locale files (en, ca, es, de, ja)
├── components/ # Web Component definitions (18 components)
├── core/ # Base classes (Element, StaticElement, ReactiveElement, FormElement)
├── lib/ # Vendored dependencies (Bootstrap 5, Bootstrap Icons, morphdom, Notyf, to-excel)
├── models/ # State management (SimplModel)
├── services/ # Application services (router, i18n, theme, modal, toast, etc.)
├── index.js # Public entrypoint
├── s4u # Component scaffolder CLI (Bash)
├── AGENTS.md # AI agent instructions
├── eslint.config.mjs # ESLint flat config
└── package.jsonGetting Started
Install
npm installMost dependencies (Bootstrap, Bootstrap Icons, morphdom, Notyf, and to-excel) are vendored in lib/. The only npm runtime dependency is ace-builds, which powers simpl-ace-editor and is loaded on demand from node_modules/ace-builds when the component is first used. ESLint is the only dev dependency.
Lint
npm run lint # Check code quality
npm run lint:fix # Auto-fix lint issuesUsage
Import the library in your HTML or JavaScript entry point:
<script type="module" src="node_modules/simpl4u/index.js"></script>All components are automatically registered as custom elements. For Electron apps, use the same import in your renderer process.
Saving disk space across multiple projects
Installing simpl4u with npm install copies the whole library into each project's node_modules, so if you have several apps that use it, the library is duplicated on disk N times.
To avoid this, install it with pnpm instead. pnpm keeps a single copy of each package version in a global store and hard-links it into every project, so all your apps share one physical copy while keeping fully isolated, working node_modules:
npm install -g pnpm
pnpm install simpl4uNo changes to your code or to simpl4u are required — pnpm installs from the same npm registry. (Note: installing the library globally with npm install -g does not help here, since import 'simpl4u/...' only resolves from a project's local node_modules. The global install is only useful for the s4u CLI.)
CLI
Simpl4u ships with the s4u CLI to scaffold a new component for your application (i.e. a project that consumes simpl4u). It creates components/{name}.js from the chosen base class and auto-registers it in components/index.js.
When you install simpl4u, npm links the CLI into node_modules/.bin, so you can run it with npx:
npx s4u component static MyComponent # extends StaticElement
npx s4u component reactive MyComponent # extends ReactiveElementInside an npm scripts entry the binary is already on the PATH, so you can call it directly:
{
"scripts": {
"generate": "s4u component reactive MyComponent"
}
}You can also install it globally to use s4u from anywhere:
npm install -g simpl4u
s4u component reactive MyComponentThe name is converted to kebab-case for the file name and custom-element tag (e.g. MyComponent → my-component.js, tag <my-component>). Generated components import their base class from the published package (e.g. import { ReactiveElement } from 'simpl4u/core/reactive-element.js';).
The
simpl-prefix is reserved for simpl4u's own built-in components. Components you scaffold for your app are named however you like — nosimpl-prefix is added.
Components
simpl-table
A dynamic, sortable, filterable data table with CRUD actions and XLS export.
<simpl-table name="users" actions="crude" context="myapp"></simpl-table>| Attribute | Type | Description |
|---|---|---|
| name | string | Model key for the table data |
| context | string | State context namespace |
| actions | string | Action flags: c(create), r(read/detail), u(update), d(delete), e(export) |
Events are emitted via subscribe():
create,update,delete,detail,export
simpl-crud
Full CRUD component wrapping simpl-table with modal forms for data entry.
<simpl-crud name="products" actions="crude"></simpl-crud>| Method | Description |
|---|---|
| setHeaders([...]) | Define column headers |
| setForm([...]) | Define form fields with type, validation, and options |
Form field definition supports: name, required, disabled, class, unique, index, type, items (for selects).
simpl-input
Text input with label, validation, and reactive binding.
<simpl-input name="email" type="email" required placeholder="Enter email"></simpl-input>simpl-textarea
Multi-line text input.
<simpl-textarea name="description" rows="5" required></simpl-textarea>simpl-ace-editor
Code editor form field powered by Ace. Binds to the model like any other form field and follows the active theme (light/dark). Ace is loaded on demand from node_modules/ace-builds the first time the component is used.
<simpl-ace-editor name="script" mode="ace/mode/javascript" label="Script"></simpl-ace-editor>| Attribute | Type | Description |
|---|---|---|
| name | string | Model key for the editor content |
| label | string | Optional field label (i18n key) |
| mode | string | Ace language mode (e.g. ace/mode/javascript). Default: ace/mode/text |
| height | string | Editor height (CSS). Default: 20rem |
| min-height | string | Minimum height (CSS). Default: 12rem |
| font-size | string | Editor font size. Default: 0.75rem |
| tab-size | number | Spaces per tab. Default: 2 |
| wrap | boolean | Enable line wrapping ("true") |
| show-print-margin | boolean | Show the print margin ("true") |
| theme-dark | string | Ace theme used in dark mode. Default: ace/theme/tomorrow_night |
| theme-light | string | Ace theme used in light mode. Default: ace/theme/textmate |
simpl-select
Dropdown select.
<simpl-select name="country" items='[{"id":"es","text":"Spain"},{"id":"de","text":"Germany"}]'></simpl-select>simpl-switch
Bootstrap toggle switch (boolean).
<simpl-switch name="active"></simpl-switch>simpl-checkboxes
Button-group checkboxes for multi-select.
<simpl-checkboxes name="roles" values="admin,user,guest"></simpl-checkboxes>simpl-combobox
Combobox with autocomplete filtering.
<simpl-combobox name="city" items='[{"id":"1","text":"Barcelona"},{"id":"2","text":"Madrid"}]'></simpl-combobox>simpl-date
HTML5 date picker.
<simpl-date name="birthdate" required></simpl-date>simpl-color
HTML5 color picker.
<simpl-color name="bgColor"></simpl-color>simpl-file
File input with optional multiple selection.
<simpl-file name="documents" multiple></simpl-file>simpl-button
Bootstrap-styled button.
<simpl-button type="primary" title="Save"></simpl-button>simpl-progress
Bootstrap progress bar.
<simpl-progress name="uploadProgress"></simpl-progress>simpl-spinner
Full-page loading overlay. Controlled via SpinnerService.
<simpl-spinner></simpl-spinner>simpl-navbar
Responsive navigation bar with language and theme toggles. Items and the colour variant are managed through NavbarService (not attributes); component properties below trigger a re-render on change.
<simpl-navbar name="My App" icon="bi-rocket" expand="md"></simpl-navbar>import { NavbarService } from './services/navbar-service.js';
NavbarService.items = [
{ id: 'home', name: 'Home', icon: 'bi-house' },
{ id: 'about', name: 'About', emmit: true },
];
NavbarService.onClick((id) => console.log('clicked', id)); // fires for emmit items| Property | Type | Description |
|---|---|---|
| name | string | App title text |
| icon | string | Bootstrap Icons class (e.g. bi-house) or an image src URL |
| iconOnly | boolean | Hide the title text, show only the icon |
| expand | string | Breakpoint at which the navbar expands (sm|md|lg|xl|xxl). Default: md |
| variant | string | Bootstrap colour variant (non-persistent default; see NavbarService.variant) |
| hideLang | boolean | Hide the language dropdown |
| hideTheme | boolean | Hide the theme switcher |
| languages | array | Override the available language list |
simpl-todo
Kanban/Trello-style board with drag-and-drop cards.
<simpl-todo></simpl-todo>simpl-combobox-list
Filterable list (internal component used by simpl-combobox).
Services
| Service | Description |
|---|---|
| RouterService | Hash-based SPA routing. view property, setView(hash), subscribe(callback). |
| LanguageService | i18n with 5 built-in locales. lang getter/setter, i18n(key, params) for translation with {{param}} interpolation, set(languages) to merge custom translations. |
| ThemeService | Light/dark theme management. theme getter/setter, switchTheme(), persists choice, detects system preference. |
| ModalService | Bootstrap modal dialogs. message(text, title), confirm(text, title) → Promise<boolean>, prompt(text, title, value) → Promise<string>, open(body, title, hideCancel). |
| ToastService | Notyf-powered notifications. success(msg), error(msg), warning(msg), info(msg). Configurable via duration (ms), dismissible, and position getters/setters. |
| SpinnerService | Spinner overlay control. show(), hide() with debounce to prevent flickering. |
| StorageService | High-level storage API wrapping StorageAdapter. saveApp(key), loadApp(key), saveUser(key), loadUser(key), saveSystem(key), loadSystem(key), saveAppModel(), loadAppModel(). |
| DatabaseService | IndexedDB record CRUD over on-demand tables. database getter/setter (per-app DB name), insert(table, record), update(table, record), get(table, id), getAll(table), where(table, predicate), remove(table, id), clear(table), count(table). |
| CryptoService | Web Crypto helpers for sensitive data: hash(text) (SHA-256), hashPassword(password), verifyPassword(password, hashed), encrypt(data, password), decrypt(payload, password). |
| FileService | Browser/Electron file operations. download(filename, data) for browser; in Electron: readFile, writeFileSync, mkdir, selectDirectory, ls, cp, rm, rmdir via IPC. |
| TextService | String utilities. unaccent(value) (remove diacritics), sanitize(value) (escape HTML), localDate(dateString) (format ISO date). |
| ConfigService | Global persistence flags. saveApp and saveUser (default true) are read by core/element.js to gate automatic view-state save/restore. |
| MessageService | Lightweight publish/subscribe message bus for decoupled (e.g. sibling) components. subscribe(topic, handler) → unsubscribe fn, unsubscribe(topic, handler), emit(topic, payload). Inside components, prefer the Element helpers on() / emit() over calling this service directly. |
| NavbarService | Owns the simpl-navbar items and colour variant. Items: items get/set, addItem(item, index), removeItem(id), updateItem(id, patch), setVisible(id, visible), getItem(id), clear(), subscribe(cb). Click bus for emmit items: onClick(cb). Variant: variant get/set (persisted), setDefaultVariant(value) (non-persistent), subscribeVariant(cb), variants list. |
Services usage
import { RouterService } from './services/router-service.js';
import { ModalService } from './services/modal-service.js';
import { ToastService } from './services/toast-service.js';
import { SpinnerService } from './services/spinner-service.js';
import { CryptoService } from './services/crypto-service.js';
import { FileService } from './services/file-service.js';
// Navigation
RouterService.subscribe((view) => console.log('Navigated to:', view));
RouterService.setView('settings');
// Modal dialogs
const confirmed = await ModalService.confirm('Delete this item?', 'Confirm');
const name = await ModalService.prompt('Enter your name:', 'Prompt', 'default');
// Notifications
ToastService.success('Saved successfully');
ToastService.error('Something went wrong');
// Configuration
ToastService.duration = 3000; // 3 seconds
ToastService.dismissible = false; // Must wait out duration
ToastService.position = { x: 'left', y: 'bottom' };
// Loading spinner
SpinnerService.show();
await doHeavyWork();
SpinnerService.hide();
// File download (browser)
FileService.download('export.xlsx', blob);
// Crypto (hash + encrypt)
const fingerprint = await CryptoService.hash('value');
const encrypted = await CryptoService.encrypt({ token: 'abc' }, 'password');
const decrypted = await CryptoService.decrypt(encrypted, 'password');
// File operations (Electron only)
const content = await FileService.readFile('/path/to/file.txt');State Management
SimplModel is a singleton reactive state container. Data is organized by context (namespaced keys).
import { SimplModel } from './models/simpl-model.js';
// Set a value
SimplModel.set('John Doe', 'name', 'userform');
// Get a value
const name = SimplModel.get('name', 'userform');
// Subscribe to changes
const unsubscribe = SimplModel.subscribe((model, property) => {
console.log(`State changed: ${property}`);
});State changes are batched via a 20ms debounce to optimize performance.
Localization
Built-in locales (located in assets/i18n/):
| Locale | Language |
|---|---|
| en | English |
| ca | Catalan |
| es | Spanish |
| de | German |
| ja | Japanese |
import { LanguageService } from './services/language-service.js';
// Get a translated string
LanguageService.i18n('confirm'); // "Are you sure?"
// With parameters
LanguageService.i18n('error-unique', { field: 'Email' });
// Change language
LanguageService.lang = 'ca';
// Add custom translations
LanguageService.set({ myKey: 'My Translation' });Themes
ThemeService manages light/dark themes using Bootstrap 5's data-bs-theme attribute:
import { ThemeService } from './services/theme-service.js';
ThemeService.theme = 'dark'; // Switch to dark mode
ThemeService.switchTheme(); // ToggleThe service automatically respects the user's prefers-color-scheme system setting on first load if no preference has been saved.
Storage
Three-tier persistence via StorageService (backed by StorageAdapter in adapters/storage-adapter.js):
| Tier | Backend | Scope |
|---|---|---|
| App | IndexedDB | Persistent across sessions |
| User | sessionStorage | Per browser tab |
| System | Electron IPC (window.api.*) | File-system persistence in Electron |
While StorageService persists the app model as a single blob, DatabaseService offers structured record CRUD over IndexedDB tables (object stores). Tables are created on demand and records use an auto-incrementing id. Because simpl4u is shared across apps, set the database name once so each app gets its own store:
import { DatabaseService } from './services/database-service.js';
DatabaseService.database = 'my-app'; // per-app DB name (set once)
const id = await DatabaseService.insert('users', { name: 'Ada' }); // auto id
const user = await DatabaseService.get('users', id);
await DatabaseService.update('users', { ...user, name: 'Ada L.' });
const all = await DatabaseService.getAll('users');
const admins = await DatabaseService.where('users', (u) => u.role === 'admin');
await DatabaseService.remove('users', id);
await DatabaseService.count('users');
await DatabaseService.clear('users');Event Binding
Components use an attribute-based event binding syntax in their templates:
<button (click)="handleSave">Save</button>This automatically calls this.handleSave(event) on the component instance.
Inter-component messaging
Components that don't share a parent/child relationship (e.g. siblings) can communicate through a publish/subscribe message bus instead of holding references to each other. The Element base class exposes two ergonomic helpers that wrap MessageService, so components never touch the service directly — mirroring how model / data / getField / setField wrap SimplModel.
this.on(topic, handler)— subscribes to a topic for the lifetime of the element. The handler is bound to the component and the subscription is automatically removed when the element leaves the DOM, so you never have to unsubscribe manually. Returns an unsubscribe function if you want to opt out early.this.emit(topic, payload)— publishes a message to every subscriber oftopic, regardless of where they sit in the DOM tree.
Topic names follow a domain:action convention (e.g. project:refresh).
// Publisher component
renewTags() {
this.emit('project:refresh');
}
// Subscriber component (a sibling)
connectedCallback() {
super.connectedCallback();
this.on('project:refresh', () => this.reloadItems());
}Subscribe in
connectedCallback(once per DOM connection), not inonReady, which runs on every render and would register a new subscription each time. Use the bus for transient signals; for shared application state prefer the reactiveSimplModel.
Roadmap
- [ ] Unit tests
- [ ] Build/bundler integration examples
- [ ] TypeScript definitions
- [ ] More built-in components (charts, trees, etc.)
- [ ] Accessibility improvements
- [ ] Documentation site
Contributing
Contributions are welcome! Please open an issue or submit a pull request.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
MIT License — see the LICENSE file for details.
