# FyneJS

[![npm version](https://img.shields.io/npm/v/fynejs)](https://www.npmjs.com/package/fynejs)
[![npm downloads (weekly)](https://img.shields.io/npm/dw/fynejs)](https://www.npmjs.com/package/fynejs)
[![npm downloads (monthly)](https://img.shields.io/npm/dm/fynejs)](https://www.npmjs.com/package/fynejs)
[![npm downloads (total)](https://img.shields.io/npm/dt/fynejs)](https://www.npmjs.com/package/fynejs)

FyneJS is a tiny, fast, zero‑dependency reactive UI framework for the browser.

## Why FyneJS

- **Tiny & fast**: zero dependencies, ~18.6KB gzipped, optimized for performance
- **Engineered reactivity**: lean, cached evaluations; fine‑grained computed invalidation
- **Smart GC**: precise cleanup for timers, observers, events, and component trees
- **Built-in SPA router**: client-side navigation with view transitions, prefetching, and lifecycle hooks
- **Browser-native TypeScript**: load `.ts` component files directly—blazing fast type stripping (~34ms for 1000 lines) with zero build tools
- **Declarative power**: rich directives and component composition without a build step
- **SEO‑friendly by design**: progressive enhancement on existing HTML; no client‑side rendering takeover, so your server content remains crawlable
- **Signals API**: lightweight child → parent messaging with payloads and bubbling

## Capabilities (with examples)

- **Built-in SPA router** with view transitions, prefetching, and navigation hooks (see [Built-in SPA Router](#built-in-spa-router) section)

- **Browser-native TypeScript**: Load `.ts` component files directly without build tools—blazing fast type stripping in ~34ms (see [Browser-Native TypeScript](#browser-native-typescript-zero-build) section)

- Declarative directives: text, HTML, show/hide, if/else, loops, model binding, events, styles, classes, transitions

  ```html
  <div x-data="{ n: 0, items: [1,2,3], open: true }">
    <button x-on:click="n++">+1</button>
    <span x-text="n"></span>

    <template x-if="open"><p>Shown</p></template>
    <!-- Transition example (class phases + end hook) -->
    <div
      x-show="open"
      x-transition:enter="transition ease-out duration-200"
      x-transition:enter-from="opacity-0 -translate-y-2"
      x-transition:enter-to="opacity-100 translate-y-0"
      x-transition:leave="transition ease-in duration-150"
      x-transition:leave-from="opacity-100 translate-y-0"
      x-transition:leave-to="opacity-0 -translate-y-2"
      x-transition:enter.after="({ el, phase, config }) => console.log('entered', config.duration)"
    >Animated block</div>
    <ul>
      <template x-for="(v,i) in items"><li>#<span x-text="i"></span>: <span x-text="v"></span></li></template>
    </ul>
  </div>
  ```

- Powerful events & modifiers: keys, buttons, touch, outside, once, passive, capture, combos, defer

  ```html
  <input x-on:keydown.enter.ctrl="save()">
  <button x-on:click.once="init()">Init once</button>
  <div x-on:click.outside="open=false">Panel</div>
  <input x-on:input.defer="recompute()"> <!-- handler runs in a microtask -->
  ```

- Model binding: inputs, checkboxes (arrays), radios, selects (multiple)

  ```html
  <input type="text" x-model="form.name">
  <input type="checkbox" value="admin" x-model="roles"> <!-- toggles 'admin' in roles -->
  <select multiple x-model="selected"> ... </select>
  ```

- Computed & watch: derived state and change observers

  ```html
  <div x-data="{ a: 2, b: 3 }" x-text="a*b"></div>
  <!-- via component API, you can also define computed and watch -->
  ```

- Lifecycle hooks: beforeMount, mounted, updated, beforeUnmount, unmounted

### Transitions (x-transition)

Animate enter/leave for `x-show` and `x-if`:

```html
<div x-data="{ open: false }">
  <button x-on:click="open = !open">Toggle</button>
  <div x-show="open" x-transition="opacity-0 transition-opacity duration-200" class="panel">Fade panel</div>
</div>
```

Granular class phases + end callback:

```html
<div
  x-data="{ open: true }"
  x-show="open"
  x-transition:enter="transition ease-out duration-300"
  x-transition:enter-from="opacity-0 scale-95"
  x-transition:enter-to="opacity-100 scale-100"
  x-transition:leave="transition ease-in duration-200"
  x-transition:leave-from="opacity-100 scale-100"
  x-transition:leave-to="opacity-0 scale-95"
  x-transition:enter.after="({ el, config }) => console.log(config.duration)"
>Modal</div>
```

`.after` / `.end` modifiers run once per completed phase (never on cancellation) and receive `{ el, phase, config }` where `config.duration` is the effective transition time including delays. For `x-if` branches, the original display value is preserved (including `display: contents` for template wrappers).

- Slot & props: lightweight component composition

- Seal and freeze: temporarily pause interactivity or lock state

  ```html
  <!-- Freeze via readonly attribute (no state changes, timers/listeners/observers paused, no renders) -->
  <component source="stats-card" readonly></component>

  <!-- Seal programmatically: allow internal state but suppress renders and side-effects -->
  <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>
  ```

  Freeze is fully read‑only (no state changes, no renders). Remove the readonly attribute (or call $seal(false) for sealed) to resume interactivity when appropriate.


- No build required: works directly in the browser; enhanced builds are optional

- `$nextTick()`: run code after DOM updates

  ```js
  this.$nextTick(()=>{/* DOM updated */})
  ```

- Event delegation: scale to large UIs

  ```html
  <script> XTool.init({ delegate: true }); </script>
  ```

- Security sandbox: restrict globals in expressions

  ```js
  XTool.init({ sandboxExpressions: true, allowGlobals: ['setTimeout'] })
  ```

## Quick start (CDN)

Include the minified build from jsDelivr or unpkg:

```html
<script src="https://cdn.jsdelivr.net/npm/fynejs@latest/dist/x-tool.min.js"></script>
<!-- or -->
<script src="https://unpkg.com/fynejs@latest/dist/x-tool.min.js"></script>
<script>
  XTool.init({ 
    debug: false,
    router: { enabled: true }  // Optional: enable SPA routing
  });
  
  // Optional: Load external components (supports .js and .ts files!)
  XTool.loadComponents([
    'components/header.js',
    'components/user-card.ts'  // TypeScript works directly in browser!
  ]);
</script>

<div x-data="{ count: 0 }">
  <button x-on:click="count++">+1</button>
  <span x-text="count"></span>
</div>
```

## TypeScript usage

There are two easy ways to use FyneJS with TypeScript:

### 1) Bundlers (recommended)

Install the package and import the API. The package exposes clean ESM/CJS entry points and ships its own types.

```ts
// main.ts
import XTool, { html } from 'fynejs';

XTool.init({ debug: false, delegate: true });

XTool.registerComponent({
  name: 'hello-world',
  data: { msg: 'Hello FyneJS' },
  // Use the tagged template for editor highlighting in TS
  template: html`<div x-text="msg"></div>`
});

// Somewhere in your HTML template or via DOM APIs:
// <component source="hello-world"></component>
```

- Works with Vite/Rollup/Webpack/ts-node/tsx without extra config.
- Types are resolved automatically via the package `exports` (no triple-slash needed).
- Tip: Import the `html` helper from `fynejs` for tagged template literals without TS errors and with IDE HTML highlighting.

### 2) CDN + types

If you’re using the CDN build in the browser and still want editor types, reference the declarations manually:

```ts
/// <reference path="./types/x-tool/types.d.ts" />
```

You can copy the file into your repo and point `typeRoots` to it, or vendor the shipped `types.d.ts`.

## Core concepts

### Auto-discovery with x-data

```html
<div x-data="{ message: 'Hello' }" x-text="message"></div>
```

### Events and modifiers

```html
<button x-on:click.prevent.stop="submit()">Save</button>
<input x-on:keydown.enter="save()">
```

### Two-way binding
### Signals (component messaging)

Signals are lightweight, component-scoped broadcasts for child → parent notifications and other local messaging. Emissions start at the current component and bubble up to ancestors; any ancestor that connected a handler for the signal name will receive the event. Bubbling can be stopped via `evt.stopPropagation()`.

Core API (available on the component context):
- `this.Signals.emit(name, payload?)`: emit and bubble upward.
- `this.Signals.connect(name, handler)`: register a handler on the current component.
- `this.Signals.disconnect(name, handler)`: unregister the handler.

Handlers receive `{ name, payload, stopPropagation }` and run with the component method context, so `this` has access to data/computed/methods.

Connect just-in-time, emit, then disconnect:

```html
<div x-data="{ log: [],
  handler(evt){ this.log = [...this.log, 'got:'+evt.payload]; },
  run(){ this.Signals.connect('hello', this.handler); this.Signals.emit('hello','x'); this.Signals.disconnect('hello', this.handler); }
}">
  <button x-on:click="run()">fire</button>
  <span x-text="log.join(',')"></span>
</div>
```

Bubbling to ancestors (no stopPropagation):

```html
<section x-data="{ heard: [], mounted(){ this.Signals.connect('hello', (evt)=>{ this.heard = [...this.heard, evt.payload]; }); } }">
  <div x-data>
    <button x-on:click="Signals.emit('hello','X')">emit</button>
  </div>
  <span x-text="heard.join(',')"></span>
</section>
```

Notes:
- Signals handlers are cleared on component destroy to avoid leaks.
- You can also connect in lifecycle to listen continuously, or connect/disconnect around a single operation.

```html
<input type="text" x-model="form.name">
<input type="checkbox" value="admin" x-model="roles"> <!-- adds/removes 'admin' in roles array -->
<select multiple x-model="selected"> ... </select>
```

### Lists and conditionals

```html
<template x-if="items.length === 0">
  <p>No items</p>
</template>

<ul>
  <template x-for="(todo, i) in todos">
    <li>
      <span x-text="todo.title"></span>
    </li>
  </template>

### Built-in SPA Router

FyneJS includes a lightweight client-side router with view transitions and navigation hooks:

```js
XTool.init({
  router: {
    enabled: true,
    transtionName: 'slide',        // CSS view transition name
    before: (to, from, info) => {
      // Check auth, analytics, etc.
      // Return false to cancel navigation
      return true;
    },
    after: (to, from, info) => {
      // Update UI, scroll, analytics
      console.log(`Navigated from ${from} to ${to}`);
    },
    error: (error, to, from) => {
      console.error('Navigation error:', error);
    },
    prefetchOnHover: true          // Smart link prefetching
  }
});
```

### Transitions (x-transition)

Animate enter/leave for `x-show` and `x-if`.

Simple toggle form:

```html
<div x-show="open" x-transition="opacity-0 transition-opacity duration-200">Fade</div>
```

Configuration object form:

```html
<div
  x-show="open"
  x-transition="{ duration: 300, easing: 'ease-out', enter: 'transition', enterFrom: 'opacity-0 scale-95', enterTo: 'opacity-100 scale-100', leave: 'transition', leaveFrom: 'opacity-100 scale-100', leaveTo: 'opacity-0 scale-95' }"
  x-transition:enter.after="({ config }) => console.log(config.duration)"
>Dialog</div>
```

Granular phases + end hook:

```html
<div
  x-show="open"
  x-transition:enter="transition ease-out duration-300"
  x-transition:enter-from="opacity-0 scale-95"
  x-transition:enter-to="opacity-100 scale-100"
  x-transition:leave="transition ease-in duration-200"
  x-transition:leave-from="opacity-100 scale-100"
  x-transition:leave-to="opacity-0 scale-95"
  x-transition:enter.after="({ el, phase, config }) => console.log(phase, config.duration, el)"
>Modal</div>
```

Notes:
- `.after` / `.end` modifiers run once per completed phase (never on cancellation)
- Handlers receive `{ el, phase, config }` where `config.duration` is the effective time including delays
- With `x-if`, original display is preserved (including `display: contents` for template wrappers)

Use `x-link` directive for SPA navigation:

```html
<nav>
  <a href="/index.html" x-link>Home</a>
  <a href="/about.html" x-link>About</a>
  <a href="/contact.html" x-link>Contact</a>
</nav>

<!-- With prefetching -->
<a href="/dashboard.html" x-link x-prefetch="hover">Dashboard</a>
```

The router intercepts link clicks, updates the URL, and loads new pages without full refreshes—perfect for multi-page apps that feel like SPAs.

### Dynamic component file loading

Load external component files (`.js` or `.ts`) with flexible loading strategies:

```js
// Preload immediately (default for string entries)
XTool.loadComponents([
  'components/stats-card.js',
  { path: 'components/chat-panel.js', mode: 'preload' }
]);

// Defer: ensure file loads before initial auto-discovery (blocks first scan)
XTool.loadComponents([
  { path: 'components/large-dashboard.js', mode: 'defer' }
]);

// Lazy: only fetch when a <component source="name"> appears in the DOM
XTool.loadComponents([
  { path: 'components/order-book.js', mode: 'lazy', name: 'order-book' },
  // name can be omitted; filename (without extension) is used
  { path: 'components/advanced-calculator.js', mode: 'lazy' }
]);

// Later in HTML (triggers lazy fetch on first encounter)
// <component source="order-book"></component>
```

Lazy mode details:
- Registration does not fetch the file until the component is actually used.
- If a matching `<component source="...">` already exists at registration time, the file is fetched in an idle callback.
- After load, auto-discovery re-runs so the component mounts automatically.

Defer mode details:
- Files are fetched before the framework performs the initial DOM scan, guaranteeing definitions are available when components are first discovered.

Return value:
`loadComponents` resolves with `{ settled, failed }` counting only immediate (preload + defer) operations; lazy entries are not counted until they actually load.

**TypeScript components work too!** FyneJS automatically strips TypeScript type annotations from `.ts` files:

```js
XTool.loadComponents([
  'components/user-card.ts',      // TypeScript file
  'components/data-chart.ts',     // TypeScript file
  'components/modal.js'           // Regular JavaScript
]);
```

### HTML Component Files

Write components in native `.html` files with full IDE syntax highlighting—no template strings needed!

```html
<!-- components/greeting.html -->
<template>
  <div class="greeting">
    <h2 x-text="message"></h2>
    <button x-on:click="greet()">Say Hello</button>
  </div>
</template>

<script setup>
const message = data('Hello World');

function greet() {
  message.value = 'Hello, FyneJS!';
}

expose({ message, greet });
onMounted(() => console.log('Mounted!'));
</script>
```

**Multiple components in one file:**

```html
<!-- components/widgets.html -->
<template name="widget-header">
  <header x-text="title"></header>
</template>
<script setup name="widget-header">
const title = data('Header');
expose({ title });
</script>

<template name="widget-footer">
  <footer x-text="copyright"></footer>
</template>
<script setup name="widget-footer">
const copyright = data('© 2025');
expose({ copyright });
</script>
```

**Load like any other component:**

```js
XTool.loadComponents([
  'components/greeting.html',
  'components/widgets.html'  // loads both widget-header & widget-footer
]);
```

Available helpers in HTML components: `data()`, `computed()`, `watch()`, `expose()`, `onMounted()`, `onBeforeMount()`, `onUnmounted()`, `onBeforeUnmount()`.

### Programmatic Component Mounting

Mount registered components on any DOM element programmatically:

```js
// Register a component
XTool.registerComponent({
  name: 'user-card',
  template: '<div><h3 x-text="name"></h3></div>',
  data: { name: 'Guest' }
});

// Mount on any element with optional props
const container = document.getElementById('dynamic-area');
const instance = XTool.mountComponent('user-card', container, {
  name: 'John Doe'
});

// Later: instance.$destroy() to unmount
```

### Element References (x-ref)

Create named references to DOM elements accessible via `$refs`:

```html
<div x-data="{ focusInput() { $refs.myInput.focus(); } }">
  <input x-ref="myInput" type="text">
  <button x-on:click="focusInput()">Focus</button>
</div>
```

- `$refs.name` returns the element (or array if multiple elements share the name)
- `$ref(name)` function form to get a ref
- `$ref(name, value)` to register any value as a ref
- Refs bubble up the component tree—child components can access parent refs

### Shorthand Attribute Binding

Three equivalent syntaxes for dynamic attributes:

```html
<div x-data="{ url: '/image.png', active: true }">
  <!-- Full syntax -->
  <img x-bind:src="url">
  
  <!-- x: shorthand -->
  <img x:src="url">
  
  <!-- : shorthand (shortest, Vue-style) -->
  <img :src="url">
  <button :disabled="!active">Click</button>
</div>
```

### DOM Helpers ($attr, $css)

Programmatically set attributes and CSS on the target element:

```html
<div x-data>
  <button x-on:click="$attr('data-clicked', true)">Mark Clicked</button>
  <div x-on:click="$css('background', 'red')">Click to color</div>
  
  <!-- Object syntax for multiple -->
  <button x-on:click="$attr({ 'data-x': 1, 'data-y': 2 })">Set Both</button>
  <div x-on:click="$css({ color: 'white', background: 'blue' })">Style Me</div>
  
  <!-- Glob pattern -->
  <div x-on:click="$attr('[width,height]', 100)">Set width & height</div>
</div>
```

### Browser-Native TypeScript (Zero Build)

**Unique to FyneJS**: Load TypeScript component files directly in the browser without any build step or compilation!

```js
// Load TypeScript components just like JavaScript
XTool.loadComponents([
  { path: 'components/user-dashboard.ts', mode: 'preload' },
  { path: 'components/analytics-chart.ts', mode: 'lazy' }
]);
```

```html
<!-- Use TypeScript components seamlessly -->
<component source="user-dashboard"></component>
<component source="analytics-chart"></component>
```

**How it works:**
- Token-based type stripping using a single-pass scanner
- Blazingly fast: ~34ms for 1000 lines of TypeScript
- Handles interfaces, types, generics, enums, access modifiers, and more
- No compilation, no waiting, no build process
- Works with simple and complex TypeScript patterns

**What gets removed:**
- Type annotations (variables, parameters, return types)
- Interface, type, namespace, and declare declarations
- Generics in functions and classes
- Import/export statements (including type-only imports)
- Non-null assertions (`!`)
- Access modifiers (public, private, protected, readonly)
- Type assertions (`as` syntax)
- Enum declarations
- `implements` clauses

**Important notes:**
- This is type *stripping*, not type *checking*—use an IDE (VS Code, WebStorm) for type safety during development
- Best suited for well-formed TypeScript code
- Simpler types work better than very complex type constructs
- Consider file size for large codebases (performance is excellent for typical component files)

**Example TypeScript component:**

```typescript
// components/user-card.ts
interface User {
  id: number;
  name: string;
  email: string;
}

XTool.registerComponent<{ user: User }>({
  name: 'user-card',
  data: {
    user: null as User | null,
    loading: false
  },
  methods: {
    async loadUser(id: number): Promise<void> {
      this.loading = true;
      const response = await fetch(`/api/users/${id}`);
      this.user = await response.json();
      this.loading = false;
    }
  },
  template: html`
    <div class="card">
      <template x-if="loading">Loading...</template>
      <template x-if="!loading && user">
        <h3 x-text="user.name"></h3>
        <p x-text="user.email"></p>
      </template>
    </div>
  `
});
```

The TypeScript code above is automatically stripped to valid JavaScript and executed in the browser—**no build step required!**

### Next tick

```js
XTool.init();
XTool.createComponent?.({ /* if using programmatic API */ });
// In methods:
this.$nextTick(() => {
  // DOM is updated
});
```

### Event delegation (large lists)

```html
<script>
XTool.init({ delegate: true });
</script>
```

Delegates `click`, `input`, `change`, `keydown`, `keyup` at the container level to reduce listeners.

### Expression sandbox (optional)

```js
XTool.init({
  sandboxExpressions: true,
  allowGlobals: ['setTimeout', 'requestAnimationFrame'] // whitelist if needed
});
```

When enabled, expressions do not see `window`/`document` unless whitelisted.

### Async component templates

```html
<component source="async-card" x-prop="{ id: 42 }"></component>
<script>
XTool.registerComponent({
  name: 'async-card',
  template: () => fetch('/card.html').then(r => r.text()),
  mounted() { /* ... */ }
});
</script>
```

Until the Promise resolves, the component element stays empty; when it resolves, the template is applied and directives are parsed.

## Components: registration, props, slots, dynamic mounting

Register once, reuse anywhere:

```html
<component source="fancy-card" x-prop="{ title: 'Hi' }"></component>

<script>
XTool.registerComponent({
  name: 'fancy-card',
  template: `
    <div class="card">
      <h3 x-text="title"></h3>
      <slot></slot>
    </div>
  `,
  data: { title: '' }
});
</script>
```

Dynamic mounting: change the `source` attribute and the framework mounts the new component and cleans up the old one automatically.

```html
<div x-data="{ src: 'fancy-card' }">
  <button x-on:click="src = src==='fancy-card' ? 'simple-card' : 'fancy-card'">Swap</button>
  <component x:source="src"></component>
</div>
```

Props are reactive; slots distribute original child nodes to `<slot>`/`<slot name="...">`.

Async templates are supported: `template` can be a string, a Promise, or a function returning a Promise.

```js
XTool.registerComponent({
  name: 'delayed-card',
  template: () => fetch('/fragments/card.html').then(r=>r.text())
});
```

## Global API (window.XTool)

```ts
XTool.init(config?: {
  container?: string;           // default: 'body'
  debug?: boolean;              // enable debug logging
  staticDirectives?: boolean;   // optimize static directives
  prefix?: string;              // default: 'x'
  delegate?: boolean;           // event delegation for performance
  sandboxExpressions?: boolean; // restrict globals in expressions
  allowGlobals?: string[];      // whitelist globals when sandboxed
  router?: {                    // SPA routing configuration
    enabled: boolean;
    transtionName?: string;     // CSS view transition name
    before?: (to: string, from: string, info: {source: string}) => boolean | Promise<boolean>;
    after?: (to: string, from: string, info: {source: string}) => void;
    error?: (error: unknown, to: string, from: string) => void;
    prefetchOnHover?: boolean;  // smart link prefetching
  }
});

XTool.directive(name: string, impl: { bind?, update?, unbind? }): void;
XTool.registerComponent({ name, data, methods, computed, propEffects, template, ... }): void;
XTool.loadComponents(sources: Array<string | { path: string; mode?: 'preload' | 'defer' | 'lazy'; name?: string }>): Promise<{ settled: number; failed: number }>;

// Optional: custom directive prefix (not hardcoded to "x")
XTool.init({ prefix: 'u' }); // use u-data, u-text, u-on:click, ...
```


## Documentation

For complete documentation, guides, and interactive examples, visit:

**[https://fynejs.com](https://fynejs.com)**

- [Getting Started Guide](https://fynejs.com/getting-started.html)
- [Directives Reference](https://fynejs.com/directives.html)
- [Components Guide](https://fynejs.com/components.html)
- [Router Documentation](https://fynejs.com/router.html)
- [TypeScript Support](https://fynejs.com/typescript.html)
- [API Reference](https://fynejs.com/api.html)
- [Examples](https://fynejs.com/examples.html)

## License

MIT
