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

// Drop it in, add attributes, get reactivity
<div x-data="{ count: 0 }">
<span x-text="count"></span>
<button x-on:click="count++">+</button>
</div>

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.

  1. beforeMount - Component created, element not yet processed.
  2. mounted - Initial directives applied, element in DOM.
  3. updated - Runs after each reactive flush that changes DOM.
  4. beforeUnmount - Right before removal/cleanup.
  5. 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 with passive: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 styling with x:class

🔥 Dynamic Component Showcase

Switch components live via changing the source attribute.

Live Swap
<component source=""></component>

⚡ High-performance Demos (Dynamic)

Stress-test reactivity; swap workloads on the fly.