Introduction
FyneJS is a lightweight reactive framework that brings immediate interactivity to your HTML with zero build steps.
Add reactivity to any HTML page with simple attributes. No compilation, no complex setup—just drop the script and start building. It ships with zero runtime dependencies (no external libraries required) so you can use it anywhere.
Quick Start
<!-- Include (via CDN or local build) -->
<script src="/dist/x-tool.min.js" defer></script>
<!-- Use anywhere -->
<div x-data="{ count: 0 }" class="p-4 border rounded">
<h3 x-text="'Count: ' + count" class="text-lg font-medium mb-2"></h3>
<button x-on:click="count++" class="px-3 py-1 bg-blue-600 text-white rounded mr-2">+1</button>
<button x-on:click="count=0" class="px-3 py-1 bg-gray-200 rounded">Reset</button>
</div>
That’s all you need. The framework auto-discovers elements with x-data
and wires directives instantly.
Features
- Tiny & Fast: Minimal runtime with efficient reactivity caching.
- Declarative Directives: Text, HTML, show/hide, if/else, loops, model binding, events, styles, classes.
- Powerful Events: Rich modifier system (keys, buttons, touch, outside, once, passive, capture, combos, defer).
- Computed: Fast, pure derived state with cache-aware getters.
- Smart GC & Auto‑clean: Per‑invocation cleanup for timers, listeners, and observers to prevent leaks.
- Lifecycle Hooks: beforeMount, mounted, updated, beforeUnmount, unmounted.
- Slot & Props: Lightweight composition via component registration.
- No Build Required: Works directly in the browser—enhanced builds optional.
- Zero Dependencies: Ships as plain JavaScript (authored in TypeScript internally).
- Plugin Friendly: Register custom directives & extend core behavior.
Zero Build
No webpack, no compilation. Works directly in the browser.
Tiny Size
Minimal footprint, optimized for fast loads and small bundles.
Composable
Build reusable components with props and reactive data flow.
Core Features
-
Declarative
x-*
attributes and{{ }}
expressions - Reusable components with slot support
-
Reactive parent-child prop passing with
x-prop
- Custom directives and extensible architecture
-
Quality of life: empty
{{ }}
are ignored; escape with\{{ ... }}
Quick Example
Getting Started
Installation
Via CDN
<script src="https://unpkg.com/fynejs@latest/dist/x-tool.min.js"></script>
Via NPM
npm install fynejs
Basic Example
<div x-data="{ msg: 'Hello' }">
<h1 x-text="msg"></h1>
<button x-on:click="msg='Hi'">Change</button>
</div>
<script src="/dist/x-tool.js"></script>
<script>XTool.init();</script>
Programmatic Component
XTool.createComponent({
data: { count: 0 },
template: `<div>
<span x-text="count"></span>
<button x-on:click="count++">+1</button>
</div>`
});
Directives Reference
Directives are special attributes that tell FyneJS how to make your HTML reactive.
They all start with x-
and each serves a specific purpose.
At-a-Glance
Directive | Purpose | Notes / Modifiers |
---|---|---|
x-data | Component state scope | Accepts object literal or function returning state |
x-text | Set textContent | Escaped, fast update |
x-html | Set innerHTML | Use with trusted content only |
x-show | Toggle visibility | Manipulates inline style display |
x-if | Conditional render | Supports x-else / x-else-if |
x-for | List / loop rendering | Provides scoped loop vars |
x-model | Two-way binding | Works with inputs, selects, textarea |
x-bind / :attr | Bind attribute/prop | Shorthand : |
x-style | Inline style binding | Object or string expression |
x-on / @event | Event listener | Rich modifiers (.once .prevent .stop .self .outside .passive .capture keys/buttons/touch/combos) |
x:class | Dynamic classes | Object / conditional |
x-prop | Pass props to child component | Works with registered components |
x-slot | Named content insertion | Lightweight slot support |
Core Directives
x-data
Defines reactive data scope for a component.
<div x-data="{ name: 'World', count: 0 }">
<!-- reactive scope -->
</div>
x-text
Sets the text content of an element.
<span x-text="name"></span>
<h1 x-text="'Hello ' + name"></h1>
x-html
Sets the innerHTML of an element. Use with caution!
<div x-html="'<strong>Bold</strong> text'"></div>
x-show
Toggles visibility with display: none
. Element stays in DOM.
<div x-show="isVisible">Toggle me!</div>
<button x-on:click="isVisible = !isVisible">Toggle</button>
x-if
Conditional rendering. Element is added/removed from DOM.
<div x-if="showContent">
This appears/disappears completely
</div>
<div x-else>
Fallback content
</div>
x-for
Loop through arrays or iterables to create repeated elements.
<ul>
<li x-for="(item, index) in items">
#\{{index}}: \{{item.name}}
</li>
</ul>
Events & Binding
x-on:event
Event listeners with modifier support. Arrow functions allowed.
<button x-on:click="count++">Simple</button>
<button x-on:click="(e) => handleClick(e)">Arrow</button>
<input x-on:keyup.enter="submit()">
<div x-on:click.outside="close()">Modal</div>
x-model
Two-way data binding for form controls.
<input x-model="name" type="text">
<input x-model="agreed" type="checkbox">
<select x-model="selected">
<option value="a">Option A</option>
</select>
x-bind:attr
or x:attr
Reactive attribute and property binding.
<img x-bind:src="imageUrl" x-bind:alt="imageAlt">
<div x:class="{ active: isActive, disabled: !enabled }">
<input x:disabled="isLoading">
x:class
Powerful class binding with string, array, and object syntax. Preserves existing classes.
<!-- Object syntax (most common) -->
<div x:class="{ 'bg-blue-500': isActive, 'text-white': isActive }">
<!-- String syntax -->
<div x:class="isActive ? 'bg-blue-500 text-white' : 'bg-gray-200'">
<!-- Array syntax -->
<div x:class="['base-class', isActive && 'active-class']">
x-style
Dynamic inline styles with object or string values.
<div x-style="{ color: textColor, fontSize: size + 'px' }">
<div x-style="'background: ' + bgColor">
Text Interpolation
\{{ expression }}
Inline reactive text within text nodes. Empty expressions ignored.
<p>Hello \{{ name }}! You have \{{ count }} items.</p>
<span>Computed: \{{ price * quantity }}</span>
<!-- Escape with backslash -->
<code>\{{ this shows literally }}</code>
Components
Build reusable, encapsulated UI pieces with reactive props, slots, and lifecycle hooks. Components help you organize complex UIs into manageable, testable units.
Lifecycle Hooks
Each component instance can tap into lifecycle hooks for setup, DOM interaction, and cleanup. Hooks run in the order shown below.
beforeMount
- Component created, element not yet processed.mounted
- Initial directives applied, element in DOM.updated
- Runs after each reactive flush that changes DOM.beforeUnmount
- Right before removal/cleanup.unmounted
- After all listeners/observers detached.
XTool.registerComponent({
name: 'timer',
template: `<div>Seconds: <span x-text="secs"></span></div>`,
makeData() { return { secs: 0, handle: null }; },
beforeMount() { console.log('init logic'); },
mounted() { this.handle = setInterval(() => { this.secs++; }, 1000); },
updated() { /* react to DOM updates if needed */ },
beforeUnmount() { clearInterval(this.handle); },
unmounted() { console.log('gone'); }
});
Basic Component Registration
Definition
XTool.registerComponent({
name: 'counter',
template: `<div class="counter">
<span x-text="count"></span>
<button x-on:click="inc">+</button>
</div>`,
makeData: (props) => ({
count: Number(props.start || 0)
}),
methods: {
inc() { this.count++; }
}
});
Usage
<component source="counter" start="5">
</component>
<!-- Multiple instances -->
<component source="counter" start="10">
</component>
Slots & Content Projection
Card Component
XTool.registerComponent({
name: 'card',
template: `<div class="card">
<header class="card-header">
<slot name="header"></slot>
</header>
<div class="card-body">
<slot></slot>
</div>
<footer class="card-footer">
<slot name="footer"></slot>
</footer>
</div>`
});
Using Slots
<component source="card">
<h4 slot="header">Card Title</h4>
<p>This is the main content that goes
into the default slot.</p>
<small slot="footer">
Last updated: today
</small>
</component>
Lifecycle Hooks & Methods
XTool.registerComponent({
name: 'timer',
template: `<div>
<div x-text="'Time: ' + seconds + 's'"></div>
<button x-on:click="toggle">\{{ running ? 'Pause' : 'Start' }}</button>
<button x-on:click="reset">Reset</button>
</div>`,
makeData: () => ({
seconds: 0,
running: false,
interval: null
}),
methods: {
toggle() {
this.running = !this.running;
if (this.running) {
this.interval = setInterval(() => this.seconds++, 1000);
} else {
clearInterval(this.interval);
}
},
reset() {
this.seconds = 0;
this.running = false;
clearInterval(this.interval);
}
},
// Lifecycle hooks
beforeMount() {
console.log('Timer about to mount');
},
mounted() {
console.log('Timer mounted');
},
beforeUnmount() {
clearInterval(this.interval);
},
unmounted() {
console.log('Timer cleaned up');
}
});
Reactive Props & Effects
Pass reactive data from parent to child components using x-prop
.
React to prop changes with propEffects
hooks.
Passing Props with x-prop
Parent Component
<div x-data="{
isOpen: false,
heading: 'Settings',
user: { name: 'John', role: 'admin' }
}">
<button x-on:click="isOpen = !isOpen">
Toggle Panel
</button>
<component
source="panel"
x-prop="{
open: isOpen,
title: heading,
user: user
}">
<p>Panel content here...</p>
</component>
</div>
Child Component
XTool.registerComponent({
name: 'panel',
template: `<div x-show="open" class="panel">
<h3 x-text="title"></h3>
<div x-text="'Welcome ' + user.name"></div>
<slot></slot>
<button x-on:click="close">Close</button>
</div>`,
makeData: () => ({
// Props will be automatically added
}),
methods: {
close() {
// Emit events or call parent methods
this.$props.open = false;
}
}
});
Prop Change Reactions with propEffects
XTool.registerComponent({
name: 'notification',
template: `<div x-show="visible"
x-bind:class="'alert alert-' + type">
<span x-text="message"></span>
<button x-on:click="hide">×</button>
</div>`,
makeData: () => ({
visible: false,
autoHideTimeout: null
}),
// React to specific prop changes
propEffects: {
message(newMessage, oldMessage) {
if (newMessage && newMessage !== oldMessage) {
this.visible = true;
this.scheduleAutoHide();
}
},
type(newType) {
console.log(`Notification type changed to: ${newType}`);
},
autoHide(shouldAutoHide) {
if (shouldAutoHide) {
this.scheduleAutoHide();
} else {
clearTimeout(this.autoHideTimeout);
}
}
},
methods: {
hide() {
this.visible = false;
clearTimeout(this.autoHideTimeout);
},
scheduleAutoHide() {
clearTimeout(this.autoHideTimeout);
if (this.autoHide) {
this.autoHideTimeout = setTimeout(() => {
this.hide();
}, 3000);
}
}
},
beforeUnmount() {
clearTimeout(this.autoHideTimeout);
}
});
Note: propEffects
only fire for parent-driven prop changes via x-prop
.
API Reference
Complete reference for FyneJS's JavaScript API, initialization options, and advanced features.
Plugins & Extensions
Extend FyneJS by registering custom directives or adding utilities—no fork required.
- Custom Directives:
XTool.directive(name, { bind, update, unbind })
// Directive example
XTool.directive('focus', {
bind(el) { el.focus(); },
update(el, value) { if (value) el.focus(); },
unbind() {}
});
// <input x-focus="isActive" />
Core API Methods
XTool.init(options?)
Initialize FyneJS and start processing directives in the DOM.
// Basic initialization
XTool.init();
// With options
XTool.init({
container: 'body', // Root element selector (default: 'body')
prefix: 'x', // Directive prefix (default: 'x')
debug: false, // Enable debug logging
staticDirectives: true // Optimize static directive performance
});
XTool.registerComponent(definition)
Register a reusable component that can be used with <component>
elements.
XTool.registerComponent({
name: 'my-component', // Required: component name
template: '<div>...</div>', // Required: HTML template string or selector
makeData: (props) => ({}), // Optional: data factory function
methods: { // Optional: component methods
doSomething() { /* */ }
},
computed: { // Optional: computed properties
fullName() { return this.first + ' ' + this.last; }
},
propEffects: { // Optional: prop change handlers
value(newVal, oldVal) { /* react to prop changes */ }
},
// Lifecycle hooks (all optional)
beforeMount() { /* before DOM insertion */ },
mounted() { /* after DOM insertion */ },
updated() { /* after reactive update */ },
beforeUnmount() { /* before removal */ },
unmounted() { /* after cleanup */ }
});
XTool.directive(name, definition)
Register custom directives to extend FyneJS's functionality.
// Custom directive example
XTool.directive('tooltip', {
bind(element, value, expression, component, modifiers, evaluator) {
// Called when directive is first bound
element.title = value;
element.style.position = 'relative';
},
update(element, value, expression, component, modifiers, evaluator) {
// Called when the bound value changes
element.title = value;
},
unbind(element, component) {
// Called when directive is removed (cleanup)
element.removeAttribute('title');
}
});
// Usage: <div x-tooltip="'Hover message'"></div>
// Advanced directive with modifiers
XTool.directive('animate', {
bind(element, value, expression, component, modifiers) {
const duration = modifiers?.duration || 300;
const easing = modifiers?.ease || 'ease-in-out';
element.style.transition = `all ${duration}ms ${easing}`;
if (value) element.classList.add(value);
},
update(element, value) {
// Toggle animation class
element.classList.toggle(value, !!value);
}
});
// Usage: <div x-animate.duration.500.ease="'fade-in'"></div>
Note: Custom directive names must NOT start with x-
(reserved for built-ins).
The framework automatically handles the x-
prefix when processing directives.
Advanced Features & Optimizations
What makes FyneJS powerful yet lightweight
🎯 Flexible Template System
FyneJS supports multiple template sources for maximum flexibility:
// HTML string template
template: '<div x-text="message"></div>'
// Element selector (any element, not just <template>)
template: '#my-template' // Uses element with id="my-template"
template: '.template-class' // Uses first element with class
// Special behavior:
// - <template> elements: Content is cloned for new instances
// - Other elements: Original element is used directly (replacement)
// - Template elements are automatically skipped during rendering
💬 Enhanced Text Interpolation
Smart mustache syntax with escape support:
<!-- Basic interpolation -->
<p>Hello \{{ name }}!</p>
<!-- Empty expressions are ignored -->
<p>\{{ }} This won't cause errors</p>
<!-- Escape with backslash to render literally -->
<p>\{{ this will show as literal mustaches }}</p>
<!-- Complex expressions -->
<p>\{{ user.name.toUpperCase() }} (\{{ items.length }} items)</p>
⚡ Performance Optimizations
- Expression Caching: Compiled expressions cached to avoid recompilation
- Context Caching: Method contexts cached to reduce proxy recreation
- Selective Parsing: Skips parsing inside <pre> and <code> blocks
- Component Boundaries: Respects x-data boundaries to avoid duplicate processing
- Class Optimization: Initial class sets cached for efficient class binding
- Batched Updates: Reactive updates batched using microtasks
- Equality Checks: Short-circuits updates when values unchanged
- Static Directives: One-time directive evaluation for static content
🧹 Automatic Memory Management
- Cleanup Functions: Automatic tracking and cleanup of resources
- Event Listeners: All listeners tracked and removed on unmount
- Component Hierarchy: Parent-child relationships for proper cleanup order
- DOM Observers: Components destroyed when removed from DOM
- Effect Disposal: Reactive effects properly disposed during cleanup
- WeakMap Usage: Prevents memory leaks in element associations
🧊 Seal & Freeze Controls
Temporarily pause interactivity or lock state when needed. Seal allows internal state but suppresses renders and external side‑effects (timers, listeners, observers). Freeze is fully read‑only—no state changes and no renders. Useful for previews, embed modes, audits, and page builders. Remove readonly
(or call $seal(false)
) to resume interactivity.
<!-- Freeze via attribute: no state changes, timers/listeners/observers paused, no renders -->
<component source="stats-card" readonly></component>
<!-- Seal programmatically: internal state can change, but no renders/effects/globals -->
<div x-data="{ paused:false, toggle(){ this.$seal(!(this.$isSealed)); this.paused = this.$isSealed; } }">
<button x-on:click="toggle()" x-text="$isSealed ? 'Resume' : 'Pause'"></button>
</div>
📦 Remarkably Compact
🚀 Enterprise Features, Minimal Footprint:
- Complete reactive system with computed properties
- Full component architecture with lifecycle hooks
- Comprehensive directive system + custom directives
- Advanced event handling with 15+ modifier types
- Template system with slot support and prop reactivity
- Smart text interpolation with escape sequences
- Performance optimizations throughout
- Automatic memory management and cleanup
💎 Delivered as a single optimized JavaScript file.
Optional Extras
Extended features available when needed (not part of core API)
XTool.createComponent(definition)
Create and mount component instances programmatically (alternative to registerComponent).
const instance = XTool.createComponent({
data: { message: 'Hello' },
template: '<div x-text="message"></div>'
});
// Mount to DOM
document.body.appendChild(instance.element);
Note: Optional features like standalone reactive objects, watchers, and computed values may be available in future extensions. The core FyneJS API focuses on the three essential methods (init, registerComponent, directive) for maximum simplicity and minimal bundle size.
Event Modifiers
Action Modifiers
.prevent
- preventDefault().stop
- stopPropagation().once
- Run only once.self
- Only if event.target is element.outside
- Trigger when click/touch occurs outside element.passive
- Adds listener withpassive:true
.capture
- Use capture phase.defer
- Run handler in a microtask after current tick
Key Modifiers
.enter
- Enter key.escape
- Escape key.space
- Spacebar.tab
- Tab key.backspace
- Backspace.delete
- Delete key.home
- Home key.end
- End key.pageup
- PageUp key.pagedown
- PageDown key.ctrl
- Ctrl key combo.shift
- Shift key combo
Complete Modifier Reference
Action Modifiers
.prevent
- preventDefault().stop
- stopPropagation().once
- Run only once.self
- Only if event.target is element.outside
- Outside element event.passive
- Passive listener.capture
- Capture phase.defer
- Defer handler via microtask
Key Modifiers
.enter
- Enter key.escape
/.esc
- Escape key.space
- Spacebar.tab
- Tab key.backspace
- Backspace.delete
/.del
- Delete.home
- Home.end
- End.pageup
- PageUp.pagedown
- PageDown
Combo Modifiers
.ctrl
- Ctrl key combo.shift
- Shift key combo.alt
- Alt key combo.meta
- Meta/Cmd key combo.alt
- Alt key combo
Arrow Key Aliases
.up
/ .arrowup
.down
/ .arrowdown
.left
/ .arrowleft
.right
/ .arrowright
Mouse Button Modifiers
.left
- Left mouse button
.middle
- Middle mouse button
.right
- Right mouse button
Touch Modifiers
.single
- Single touch point
.multi
- Multiple touch points
.defer Example
<input x-on:input.defer="recompute()" placeholder="Type to recompute after microtask" />
Advanced Examples
<!-- Combo modifiers -->
<input x-on:keyup.ctrl.enter="submitForm">
<div x-on:keydown.ctrl.shift.s="saveAs">
<!-- Multiple modifiers -->
<button x-on:click.prevent.stop.once="handleClick">
<!-- Arrow key navigation -->
<div x-on:keydown.up="moveUp" x-on:keydown.down="moveDown">
<!-- Mouse button specific -->
<div x-on:mousedown.right.prevent="showContextMenu">
<!-- Outside click (closes when clicking elsewhere) -->
<div x-data="{ open: true }" class="relative">
<button x-on:click="open=!open" class="px-2 py-1 bg-gray-200 rounded">Toggle</button>
<div x-show="open" x-on:click.outside="open=false" class="absolute top-full left-0 mt-1 bg-white border p-2 rounded shadow">Menu</div>
</div>
<!-- Passive/capture example (wheel passive, capture for early keydown) -->
<div x-on:wheel.passive="handleWheel" x-on:keydown.capture.ctrl.s.prevent="save" tabindex="0" class="p-2 border rounded">Scrollable Area</div>
<!-- Touch events -->
<div x-on:touchstart.single="handleSingleTouch"
x-on:touchstart.multi="handleMultiTouch">
Advanced Live Examples
🚀 Pushing FyneJS to its limits! These advanced examples showcase the full power and flexibility of FyneJS's reactive system, component architecture, and directive capabilities.
Simple Counter
Dynamic Classes
🔥 Dynamic Component Showcase
Switch components live via changing the source
attribute.
⚡ High-performance Demos (Dynamic)
Stress-test reactivity; swap workloads on the fly.