This commit is contained in:
@@ -2,18 +2,19 @@
|
||||
|
||||
* **Introduction**
|
||||
* [Installation](install.md)
|
||||
* [Examples](examples.md)
|
||||
* [Vite Plugin](vite/plugin.md)
|
||||
|
||||
* **API Reference**
|
||||
* [Quick Start](api/quick.md)
|
||||
* [Signals & Proxies](api/signal.md)
|
||||
* [Watch](api/watch.md)
|
||||
* [If](api/if.md)
|
||||
* [For](api/for.md)
|
||||
* [Router](api/router.md)
|
||||
* [Mount](api/mount.md)
|
||||
* [Tag](api/html.md)
|
||||
* [watch](api/watch.md)
|
||||
* [when](api/when.md)
|
||||
* [each](api/each.md)
|
||||
* [router](api/router.md)
|
||||
* [fx](api/fx.md)
|
||||
* [req](api/req.md)
|
||||
* [mount](api/mount.md)
|
||||
* [h](api/h.md)
|
||||
* [Tags](api/tags.md)
|
||||
* [Global Store](api/global.md)
|
||||
* [JSX Style](api/jsx.md)
|
||||
160
docs/api/each.md
Normal file
160
docs/api/each.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# Reactive Lists: `each( )`
|
||||
|
||||
The `each` function is a high‑performance keyed list renderer. It maps a reactive array to DOM nodes and surgically updates only the items that changed (added, removed, or reordered). Unlike a simple `.map()`, `each` reuses DOM nodes and preserves internal state.
|
||||
|
||||
## Function Signature
|
||||
|
||||
```typescript
|
||||
each(
|
||||
source: Signal<any[]> | (() => any[]) | any[],
|
||||
itemFn: (item: any, index: number) => Node | (() => Node),
|
||||
keyFn?: (item: any, index: number) => string | number
|
||||
): HTMLElement
|
||||
```
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **`source`** | `Signal`, `() => any[]`, or `any[]` | Yes | The reactive array to iterate over. |
|
||||
| **`itemFn`** | `(item, index) => Node` | Yes | Returns a DOM node (or a function that returns a node) for each item. |
|
||||
| **`keyFn`** | `(item, index) => string/number` | No | Extracts a unique key. Default: `item?.id ?? index`. |
|
||||
|
||||
**Returns:** A `div` with `style="display: contents"` that contains the live list.
|
||||
|
||||
---
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### 1. Basic Keyed List (Recommended)
|
||||
|
||||
Always provide a unique `id` as the key. This allows SigPro to reuse DOM nodes when the list is reordered or filtered.
|
||||
|
||||
```javascript
|
||||
const users = $([
|
||||
{ id: 1, name: "Alice" },
|
||||
{ id: 2, name: "Bob" }
|
||||
]);
|
||||
|
||||
ul({ class: "list" }, [
|
||||
each(users,
|
||||
(user) => li({ class: "p-2" }, user.name),
|
||||
(user) => user.id // stable unique key
|
||||
)
|
||||
]);
|
||||
```
|
||||
|
||||
### 2. Automatic Key (Simple Lists)
|
||||
|
||||
If you omit the `keyFn`, `each` defaults to `item?.id ?? index`. For primitive arrays or objects without an `id`, the index is used.
|
||||
|
||||
```javascript
|
||||
const tags = $(["Tech", "JS", "Web"]);
|
||||
|
||||
div({ class: "flex gap-1" }, [
|
||||
each(tags, (tag) => span({ class: "badge" }, tag))
|
||||
// key defaults to index (0,1,2) – fine for static order
|
||||
]);
|
||||
```
|
||||
|
||||
### 3. Dynamic Content Using Functions
|
||||
|
||||
If your `itemFn` returns a **function**, that function is re‑executed every time the item’s data changes (but the node is reused).
|
||||
|
||||
```javascript
|
||||
const todos = $([
|
||||
{ id: 1, text: "Learn SigPro", done: false }
|
||||
]);
|
||||
|
||||
each(todos,
|
||||
(todo) => div([
|
||||
input({ type: "checkbox", checked: () => todo.done, onInput: e => todo.done = e.target.checked }),
|
||||
span(() => todo.done ? s(todo.text) : todo.text)
|
||||
]),
|
||||
(todo) => todo.id
|
||||
);
|
||||
```
|
||||
|
||||
### 4. Source as a Plain Array or Function
|
||||
|
||||
`source` can be a plain array (non‑reactive) or a function that returns an array – it will still react to changes if signals are read inside the function.
|
||||
|
||||
```javascript
|
||||
const filter = $("all");
|
||||
|
||||
const filteredTodos = () => {
|
||||
const all = todos();
|
||||
if (filter() === "active") return all.filter(t => !t.done);
|
||||
return all;
|
||||
};
|
||||
|
||||
each(filteredTodos, (todo) => li(todo.text), (todo) => todo.id);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How It Works (Reconciliation)
|
||||
|
||||
When the `source` changes, `each`:
|
||||
|
||||
1. **Compares keys** between the old and new items using the `keyFn`.
|
||||
2. **Reuses existing DOM nodes** for keys that stay the same.
|
||||
3. **Moves nodes** if order changed (no recreation).
|
||||
4. **Creates new nodes** for new keys.
|
||||
5. **Destroys nodes** for removed keys – cleans up all effects, event listeners, and child components.
|
||||
|
||||
> This is much more efficient than destroying and rebuilding the whole list on every update.
|
||||
|
||||
---
|
||||
|
||||
## Performance Tips
|
||||
|
||||
- **Stable keys** – Use a real `id` (like a database primary key). Avoid `Math.random()` or array `index` for lists that can be reordered.
|
||||
- **State preservation** – If a list item contains an input or a local state, using a stable key ensures that state is preserved even when the list is filtered or sorted.
|
||||
- **Lazy item functions** – If an item is expensive to render, wrap it in a function: `() => ExpensiveComponent(item)`. The component is only created when the item actually appears in the DOM.
|
||||
|
||||
---
|
||||
|
||||
## Summary Comparison
|
||||
|
||||
| Feature | Standard `Array.map` | SigPro `each` |
|
||||
| :--- | :--- | :--- |
|
||||
| **Re‑renders on change** | Re‑creates entire list | Only adds/removes/moves changed items |
|
||||
| **DOM nodes** | New nodes every time | **Reused via keys** |
|
||||
| **Memory cleanup** | Manual (or leak) | **Automatic** (destroy on removal) |
|
||||
| **Internal state per item** | Lost on every update | **Preserved** (if key stable) |
|
||||
| **Reactivity** | None (manual re‑render) | Built‑in, fine‑grained |
|
||||
|
||||
---
|
||||
|
||||
## Complete Example
|
||||
|
||||
```javascript
|
||||
const items = $([
|
||||
{ id: 1, name: "Apple", price: 1.2 },
|
||||
{ id: 2, name: "Banana", price: 0.8 }
|
||||
]);
|
||||
|
||||
const addItem = () => {
|
||||
const newId = Date.now();
|
||||
items([...items(), { id: newId, name: `Item ${newId}`, price: 1.0 }]);
|
||||
};
|
||||
|
||||
const removeItem = (id) => {
|
||||
items(items().filter(i => i.id !== id));
|
||||
};
|
||||
|
||||
const App = () =>
|
||||
div([
|
||||
button({ onClick: addItem }, "Add item"),
|
||||
ul(
|
||||
each(items,
|
||||
(item) => li([
|
||||
span(`${item.name} – $${item.price}`),
|
||||
button({ onClick: () => removeItem(item.id) }, "X")
|
||||
]),
|
||||
(item) => item.id
|
||||
)
|
||||
)
|
||||
]);
|
||||
|
||||
mount(App, '#app');
|
||||
```
|
||||
@@ -1,83 +0,0 @@
|
||||
# Reactive Lists: `For( )`
|
||||
|
||||
The `For` function is a high-performance list renderer. It maps an array (or a Signal containing an array) to DOM nodes. Unlike a simple `.map()`, `For` is **keyed**, meaning it only updates, moves, or deletes the specific items that changed.
|
||||
|
||||
## Function Signature
|
||||
|
||||
```typescript
|
||||
For(
|
||||
source: Signal<any[]> | Function | any[],
|
||||
render: (item: any, index: number) => HTMLElement,
|
||||
keyFn?: (item: any, index: number) => string | number
|
||||
): HTMLElement
|
||||
```
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **`source`** | `Signal` | Yes | The reactive array to iterate over. |
|
||||
| **`render`** | `Function` | Yes | A function that returns a component or Node for each item. |
|
||||
| **`keyFn`** | `Function` | **No** | A function to extract a **unique ID**. If omitted, it defaults to the `index`. |
|
||||
|
||||
**Returns:** A `div` element with `display: contents` containing the live list.
|
||||
|
||||
---
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### 1. Basic Keyed List (Recommended)
|
||||
Always use a unique property (like an `id`) as a key to ensure SigPro doesn't recreate nodes unnecessarily when reordering or filtering.
|
||||
|
||||
```javascript
|
||||
const users = $([
|
||||
{ id: 1, name: "Alice" },
|
||||
{ id: 2, name: "Bob" }
|
||||
]);
|
||||
|
||||
Ul({ class: "list" }, [
|
||||
For(users,
|
||||
(user) => Li({ class: "p-2" }, user.name),
|
||||
(user) => user.id // Stable and unique key
|
||||
)
|
||||
]);
|
||||
```
|
||||
|
||||
### 2. Simplified Usage (Automatic Key)
|
||||
If you omit the third parameter, `For` will automatically use the array index as the key. This is ideal for simple lists that don't change order frequently.
|
||||
|
||||
```javascript
|
||||
const tags = $(["Tech", "JS", "Web"]);
|
||||
|
||||
// No need to pass keyFn if the index is sufficient
|
||||
Div({ class: "flex gap-1" }, [
|
||||
For(tags, (tag) => Badge(tag))
|
||||
]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How it Works (The Reconciliation)
|
||||
|
||||
When the `source` signal changes, `For` performs the following steps:
|
||||
|
||||
1. **Key Diffing**: It compares the new keys with the previous ones stored in an internal `Map`.
|
||||
2. **Node Reuse**: If a key already exists, the DOM node is **reused** and moved to its new position. No new elements are created.
|
||||
3. **Physical Cleanup**: If a key disappears from the list, SigPro calls `.destroy()` to stop reactivity and physically removes the node from the DOM to prevent memory leaks.
|
||||
|
||||
---
|
||||
|
||||
## Performance Tips
|
||||
|
||||
* **Stable Keys**: Never use `Math.random()` as a key. This will force SigPro to destroy and recreate the entire list on every update, killing performance.
|
||||
* **State Preservation**: If your list items have internal state (like an input with text), `For` ensures that state is preserved even if the list is reordered, as long as the key (`id`) remains the same.
|
||||
|
||||
---
|
||||
|
||||
## Summary Comparison
|
||||
|
||||
| Feature | Standard `Array.map` | SigPro `For` |
|
||||
| :--- | :--- | :--- |
|
||||
| **Re-render** | Re-renders everything | Only updates changes |
|
||||
| **DOM Nodes** | Re-created every time | **Reused via Keys** |
|
||||
| **Memory** | Potential leaks | **Automatic Cleanup** |
|
||||
| **State** | Lost on re-render | **Preserved per item** |
|
||||
| **Ease of Use** | Manual logic required | **Optional (fallback to index)** |
|
||||
182
docs/api/fx.md
Normal file
182
docs/api/fx.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# Animation Helper: `fx( )`
|
||||
|
||||
The `fx` function applies simple **enter animations** to DOM elements. You can either use a predefined CSS keyframes animation or declare inline transition effects (scale, slide, rotate, blur). It is designed to be used when dynamically creating elements – especially inside `when` or `each` branches.
|
||||
|
||||
## Function Signature
|
||||
|
||||
```typescript
|
||||
fx(
|
||||
options: {
|
||||
name?: string; // CSS keyframes animation name (will append '-in')
|
||||
duration?: number; // Animation duration in ms (default: 200)
|
||||
scale?: boolean; // Start with scale(0.95) → none
|
||||
slide?: boolean; // Start with translateY(-10px) → none
|
||||
rotate?: boolean; // Start with rotate(-2deg) → none
|
||||
blur?: boolean; // Start with blur(4px) → none
|
||||
},
|
||||
child: Node | (() => Node)
|
||||
): Node
|
||||
```
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **`options`** | `object` | Yes | Animation configuration. |
|
||||
| **`options.name`** | `string` | No | Name of a CSS `@keyframes` animation. The actual animation name becomes `${name}-in`. |
|
||||
| **`options.duration`** | `number` | No | Duration in milliseconds (default `200`). |
|
||||
| **`options.scale`** | `boolean` | No | Add a scale transform from `0.95` to `none`. |
|
||||
| **`options.slide`** | `boolean` | No | Add a vertical slide from `translateY(-10px)` to `none`. |
|
||||
| **`options.rotate`** | `boolean` | No | Add a small rotation from `rotate(-2deg)` to `none`. |
|
||||
| **`options.blur`** | `boolean` | No | Add a blur filter from `blur(4px)` to `none`. |
|
||||
| **`child`** | `Node` or `() => Node` | Yes | The element to animate. If a function is passed, it is called to obtain the node. |
|
||||
|
||||
**Returns:** The same DOM node (or the child if it is not a `Node`), after applying the animation setup.
|
||||
|
||||
---
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### 1. Named CSS Keyframes Animation
|
||||
|
||||
Define a `@keyframes` rule in your CSS, for example:
|
||||
|
||||
```css
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
```
|
||||
|
||||
Then apply it with `fx`:
|
||||
|
||||
```javascript
|
||||
const MyComponent = () =>
|
||||
fx({ name: "fade", duration: 300 },
|
||||
div("I will fade in")
|
||||
);
|
||||
```
|
||||
|
||||
> The animation name used is `${name}-in`. In this example: `fade-in`.
|
||||
|
||||
### 2. Inline Transition (Scale + Opacity)
|
||||
|
||||
No CSS keyframes needed. The element starts with `opacity: 0` and `transform: scale(0.95)`, then transitions to `opacity: 1` and `transform: none`.
|
||||
|
||||
```javascript
|
||||
fx({ scale: true, duration: 200 },
|
||||
button({ onClick: () => alert("Hi") }, "Click me")
|
||||
);
|
||||
```
|
||||
|
||||
### 3. Combining Multiple Effects
|
||||
|
||||
You can combine `scale`, `slide`, `rotate`, and `blur` at the same time.
|
||||
|
||||
```javascript
|
||||
fx({ scale: true, slide: true, blur: true, duration: 250 },
|
||||
div({ class: "card" }, "Smooth enter")
|
||||
);
|
||||
```
|
||||
|
||||
### 4. Using with `when` (Conditional Rendering)
|
||||
|
||||
Wrap the branch content with `fx` to animate entering elements.
|
||||
|
||||
```javascript
|
||||
when(show,
|
||||
() => fx({ slide: true },
|
||||
div("This slides in when visible")
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
### 5. Using a Function as Child
|
||||
|
||||
If the element is created inside a function (e.g. to avoid recreation until needed), pass a function that returns the node.
|
||||
|
||||
```javascript
|
||||
fx({ scale: true },
|
||||
() => div("Lazy created and then animated")
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What Happens Under the Hood
|
||||
|
||||
### With `name` (CSS animation)
|
||||
|
||||
- Sets `el.style.animation = `${name}-in ${duration}ms``.
|
||||
- The element animates according to your keyframes.
|
||||
- No further inline style changes are applied.
|
||||
|
||||
### Without `name` (transition effects)
|
||||
|
||||
- Sets `el.style.transition = `all ${duration}ms ease``.
|
||||
- Sets initial `opacity: 0`.
|
||||
- Applies initial transforms (`scale`, `slide`, `rotate`) if selected.
|
||||
- Applies initial `filter: blur(4px)` if `blur: true`.
|
||||
- In the next animation frame (via `requestAnimationFrame`), sets:
|
||||
- `opacity: 1`
|
||||
- `transform: none`
|
||||
- `filter: none`
|
||||
- The element transitions smoothly from the start state to the final state.
|
||||
|
||||
> **Important:** The element must be **in the DOM** when the animation starts. `fx` does **not** automatically mount the node – you must already have appended it or be about to mount it. In practice, when you call `fx` inside a component that is being mounted, the element will be added to the DOM shortly after, and the animation runs correctly.
|
||||
|
||||
---
|
||||
|
||||
## Complete Example
|
||||
|
||||
```javascript
|
||||
const App = () =>
|
||||
div([
|
||||
fx({ name: "fade", duration: 400 },
|
||||
h1("Welcome to SigPro")
|
||||
),
|
||||
fx({ scale: true, slide: true, duration: 250 },
|
||||
button({ onClick: () => alert("Animated!") }, "Animated button")
|
||||
)
|
||||
]);
|
||||
|
||||
mount(App, "#app");
|
||||
```
|
||||
|
||||
With accompanying CSS:
|
||||
|
||||
```css
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- `fx` is **not** required for basic reactivity – it is purely a visual helper for enter animations.
|
||||
- For exit animations (when an element is removed), use CSS transitions on the element itself combined with `when` – or consider adding a wrapper that delays removal. SigPro does not include built‑in exit animations.
|
||||
- The function returns the same node you passed, so you can inline it inside `h` or tag helpers:
|
||||
|
||||
```javascript
|
||||
div([
|
||||
fx({ scale: true }, span("Hello"))
|
||||
])
|
||||
```
|
||||
|
||||
- If `child` is not a DOM node (e.g., a string or number), `fx` returns it unchanged – no animation is applied.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Option | Effect |
|
||||
| :--- | :--- |
|
||||
| `name` | Uses `@keyframes ${name}-in` CSS animation. |
|
||||
| `duration` | Controls animation/transition length (ms). |
|
||||
| `scale` | Start scale `0.95` → `none`. |
|
||||
| `slide` | Start `translateY(-10px)` → `none`. |
|
||||
| `rotate` | Start `rotate(-2deg)` → `none`. |
|
||||
| `blur` | Start `blur(4px)` → `none`. |
|
||||
|
||||
Combine options to create smooth, modern entrance effects without writing extra CSS.
|
||||
@@ -1,64 +1,67 @@
|
||||
# Global State Management: Atomic & Modular
|
||||
|
||||
SigPro leverages the native power and efficiency of **Signals** to create robust global stores with **0% complexity**. While other frameworks force you into heavy libraries and rigid boilerplate (Redux, Pinia, or Svelte Stores), SigPro treats "The Store" as a simple architectural choice: **defining a Signal outside of a component.**
|
||||
SigPro leverages the native power and efficiency of **signals** to create robust global stores with **zero complexity**. While other frameworks force you into heavy libraries and rigid boilerplate (Redux, Pinia, or Svelte stores), SigPro treats “the store” as a simple architectural choice: **defining a signal outside of a component.**
|
||||
|
||||
## Modular Organization (Zero Constraints)
|
||||
|
||||
You are not restricted to a single `store.js`. You can organize your state by **feature**, **domain**, or **page**. Since a SigPro store is just a standard JavaScript module exporting Signals, you can name your files whatever you like (`auth.js`, `cart.js`, `settings.js`) to keep your logic clean.
|
||||
You are not restricted to a single `store.js`. You can organize your state by **feature**, **domain**, or **page**. Since a SigPro store is just a standard JavaScript module exporting signals, you can name your files whatever you like (`auth.js`, `cart.js`, `settings.js`) to keep your logic clean.
|
||||
|
||||
### 1. File-Based Stores (`<any-name>.js`)
|
||||
Creating a dedicated file allows you to export only what you need. This modularity ensures **Tree Shaking** works perfectly—you never load state that isn't imported.
|
||||
### 1. File‑Based Stores (`<any-name>.js`)
|
||||
|
||||
Creating a dedicated file allows you to export only what you need. This modularity ensures **tree shaking** works perfectly – you never load state that isn’t imported.
|
||||
|
||||
```javascript
|
||||
// auth.js
|
||||
import SigPro from "sigpro";
|
||||
import { $ } from 'sigpro'; // or just rely on global `$` after import
|
||||
|
||||
// A simple global signal
|
||||
export const user = $({ name: "Guest", loggedIn: false });
|
||||
|
||||
// A persistent global signal (auto-syncs with localStorage via native key)
|
||||
// A persistent global signal (auto‑syncs with localStorage)
|
||||
export const theme = $("light", "app-theme-pref");
|
||||
|
||||
// A computed global signal that reacts to the 'user' signal
|
||||
export const welcomeMessage = $(() => `Welcome back, ${user().name}!`);
|
||||
```
|
||||
|
||||
### 2. Cross-Component Consumption
|
||||
Once exported, these signals act as a **Single Source of Truth**. SigPro ensures that if a signal changes in one file, every component importing it across the entire app updates **atomically** without a full re-render.
|
||||
### 2. Cross‑Component Consumption
|
||||
|
||||
Once exported, these signals act as a **single source of truth**. SigPro ensures that if a signal changes in one file, every component importing it across the entire app updates **atomically** without a full re‑render.
|
||||
|
||||
```javascript
|
||||
// Profile.js
|
||||
import { user } from "./auth.js";
|
||||
|
||||
const Profile = () => Div([
|
||||
H2(user().name),
|
||||
Button({ onclick: () => user({ name: "John Doe", loggedIn: true }) }, "Log In")
|
||||
const Profile = () => div([
|
||||
h2(() => user().name),
|
||||
button({ onclick: () => user({ name: "John Doe", loggedIn: true }) }, "Log In")
|
||||
]);
|
||||
|
||||
// Navbar.js
|
||||
import { welcomeMessage, theme } from "./auth.js";
|
||||
|
||||
const Navbar = () => Nav({ class: theme }, [
|
||||
Span(welcomeMessage)
|
||||
const Navbar = () => nav({ class: () => theme() }, [
|
||||
span(() => welcomeMessage())
|
||||
]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Why SigPro Stores are Superior
|
||||
## Why SigPro Stores Are Superior
|
||||
|
||||
| Feature | SigPro | Redux / Pinia / Svelte |
|
||||
| :--- | :--- | :--- |
|
||||
| **Boilerplate** | **0%** (Just a variable) | High (Actions, Reducers, Wrappers) |
|
||||
| **Organization** | **Unlimited** (Any filename) | Often strictly "Store" or "Actions" |
|
||||
| **Persistence** | **Native** (Just add a key) | Requires Middleware / Plugins |
|
||||
| **Learning Curve** | **Instant** | Steep / Complex |
|
||||
| **Bundle Size** | **0KB** (Part of the core) | 10KB - 30KB+ |
|
||||
| :-------------------- | :---------------------------- | :------------------------------ |
|
||||
| **Boilerplate** | **0%** (just a variable) | High (actions, reducers, stores)|
|
||||
| **Organization** | **Unlimited** (any filename) | Often strictly “store” files |
|
||||
| **Persistence** | **Native** (just add a key) | Requires middleware / plugins |
|
||||
| **Learning Curve** | **Instant** | Steep / complex |
|
||||
| **Bundle Size** | **0KB** (part of core) | 10KB – 30KB+ |
|
||||
|
||||
---
|
||||
|
||||
## The "Persistence" Advantage
|
||||
The magic of SigPro’s `$(value, "key")` is that it works identically for local and global states. By simply adding a second argument, your Modular Store survives browser refreshes automatically. No manual `localStorage.getItem` or `JSON.parse` logic is ever required.
|
||||
## The Persistence Advantage
|
||||
|
||||
The magic of SigPro’s `$(value, "key")` is that it works identically for local and global states. By simply adding a second argument, your modular store survives browser refreshes automatically. No manual `localStorage.getItem` or `JSON.parse` logic is ever required.
|
||||
|
||||
```javascript
|
||||
// This single line creates a global, reactive,
|
||||
@@ -69,7 +72,68 @@ export const cart = $([], "session-cart");
|
||||
---
|
||||
|
||||
## Summary of Scopes
|
||||
* **Local Scope:** Signal defined **inside** a component. Unique to every instance created.
|
||||
* **Module Scope:** Signal defined **outside** a component (same file). Shared by all instances within that specific file.
|
||||
* **Global Scope:** Signal defined in a **separate file**. Shared across the entire application by any importing module.
|
||||
* **Persistent Scope:** Any Signal defined with a **key**. Shared globally and remembered after a page reload.
|
||||
|
||||
| Scope | Definition | Behaviour |
|
||||
| :-------------- | :-------------------------------------------------------------- | :-------------------------------------------- |
|
||||
| **Local** | Signal defined **inside** a component | Unique to every component instance |
|
||||
| **Module** | Signal defined **outside** a component (same file) | Shared by all instances within that file |
|
||||
| **Global** | Signal defined in a **separate file** and imported | Shared across the entire application |
|
||||
| **Persistent** | Any Signal defined with a **key** (e.g., `$([], "cart")`) | Shared globally and persisted in `localStorage` |
|
||||
|
||||
---
|
||||
|
||||
## Complete Example – Todo Store
|
||||
|
||||
```javascript
|
||||
// store/todos.js
|
||||
import { $ } from 'sigpro';
|
||||
|
||||
export const todos = $([], "todos");
|
||||
export const filter = $("all");
|
||||
|
||||
export const addTodo = (text) => {
|
||||
todos([...todos(), { id: Date.now(), text, done: false }]);
|
||||
};
|
||||
|
||||
export const toggleTodo = (id) => {
|
||||
todos(todos().map(t => t.id === id ? { ...t, done: !t.done } : t));
|
||||
};
|
||||
|
||||
export const filteredTodos = $(() => {
|
||||
const all = todos();
|
||||
if (filter() === "active") return all.filter(t => !t.done);
|
||||
if (filter() === "completed") return all.filter(t => t.done);
|
||||
return all;
|
||||
});
|
||||
```
|
||||
|
||||
```javascript
|
||||
// components/TodoApp.js
|
||||
import { todos, filter, addTodo, toggleTodo, filteredTodos } from "../store/todos.js";
|
||||
|
||||
const TodoApp = () =>
|
||||
div({ class: "todo-app" }, [
|
||||
input({ placeholder: "Add todo...", onKeyDown: (e) => {
|
||||
if (e.key === "Enter" && e.target.value) {
|
||||
addTodo(e.target.value);
|
||||
e.target.value = "";
|
||||
}
|
||||
}}),
|
||||
div({ class: "filters" }, [
|
||||
button({ onClick: () => filter("all") }, "All"),
|
||||
button({ onClick: () => filter("active") }, "Active"),
|
||||
button({ onClick: () => filter("completed") }, "Completed")
|
||||
]),
|
||||
ul(
|
||||
each(filteredTodos,
|
||||
(todo) => li([
|
||||
input({ type: "checkbox", checked: () => todo.done, onInput: () => toggleTodo(todo.id) }),
|
||||
span(() => todo.done ? s(todo.text) : todo.text)
|
||||
]),
|
||||
(todo) => todo.id
|
||||
)
|
||||
)
|
||||
]);
|
||||
|
||||
mount(TodoApp, "#app");
|
||||
```
|
||||
|
||||
159
docs/api/h.md
Normal file
159
docs/api/h.md
Normal file
@@ -0,0 +1,159 @@
|
||||
# Hyperscript Function: `h( )`
|
||||
|
||||
The `h` function is the **core DOM builder** of SigPro. It creates DOM elements from a tag name, props, and children. While the global tag helpers (`div()`, `button()`, etc.) are built on top of `h`, you may need `h` directly for dynamic tag names or when you prefer an explicit function style.
|
||||
|
||||
## Function Signature
|
||||
|
||||
```typescript
|
||||
h(
|
||||
tag: string | Function,
|
||||
props?: object | Node | any[],
|
||||
children?: any
|
||||
): Node
|
||||
```
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| **`tag`** | `string` or `Function` | HTML tag name (e.g., `"div"`) or a component function. |
|
||||
| **`props`** | `object` | Optional. Attributes, event handlers, refs, etc. If not an object, it becomes `children`. |
|
||||
| **`children`** | `any` | Optional. Text, nodes, arrays, or reactive functions. |
|
||||
|
||||
**Returns:** A DOM node (or an array of nodes when the tag is a component that returns an array).
|
||||
|
||||
---
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### 1. Basic Element Creation
|
||||
|
||||
```javascript
|
||||
// Simple div with text
|
||||
h('div', {}, 'Hello world');
|
||||
|
||||
// With attributes
|
||||
h('button', { class: 'btn', onclick: () => alert('clicked') }, 'Click me');
|
||||
```
|
||||
|
||||
### 2. Nested Children
|
||||
|
||||
Children can be a single node, an array, or a function.
|
||||
|
||||
```javascript
|
||||
h('div', { class: 'container' }, [
|
||||
h('h1', {}, 'Title'),
|
||||
h('p', {}, 'Paragraph text')
|
||||
]);
|
||||
```
|
||||
|
||||
### 3. Reactive Children
|
||||
|
||||
Pass a **function** as a child – it will be re‑evaluated whenever any signal inside changes, and the DOM will be patched surgically.
|
||||
|
||||
```javascript
|
||||
const count = $(0);
|
||||
|
||||
h('div', {}, [
|
||||
h('p', {}, () => `Count: ${count()}`),
|
||||
h('button', { onclick: () => count(count() + 1) }, '+1')
|
||||
]);
|
||||
```
|
||||
|
||||
### 4. Reactive Attributes
|
||||
|
||||
Pass a function as an attribute value to keep it dynamic.
|
||||
|
||||
```javascript
|
||||
const theme = $('dark');
|
||||
|
||||
h('div', { class: () => `box ${theme()}` }, 'Themed box');
|
||||
```
|
||||
|
||||
### 5. Two‑Way Binding
|
||||
|
||||
Assign a signal directly to `value` or `checked` on form elements – SigPro automatically syncs both ways.
|
||||
|
||||
```javascript
|
||||
const name = $('');
|
||||
|
||||
h('input', {
|
||||
type: 'text',
|
||||
value: name, // two-way binding
|
||||
placeholder: 'Your name'
|
||||
});
|
||||
h('p', {}, () => `Hello, ${name()}`);
|
||||
```
|
||||
|
||||
### 6. Component Functions as `tag`
|
||||
|
||||
You can pass a component function directly to `h`. SigPro will execute it with the provided props and an `emit` helper for custom events.
|
||||
|
||||
```javascript
|
||||
const Button = (props, { children }) =>
|
||||
h('button', { class: 'btn', onclick: props.onClick }, children);
|
||||
|
||||
const App = () =>
|
||||
h('div', {}, [
|
||||
h(Button, { onClick: () => alert('clicked') }, 'Custom button')
|
||||
]);
|
||||
```
|
||||
|
||||
### 7. SVG Elements
|
||||
|
||||
Use `h` with SVG tag names – SigPro automatically applies the correct namespace.
|
||||
|
||||
```javascript
|
||||
h('svg', { width: 100, height: 100 }, [
|
||||
h('circle', { cx: 50, cy: 50, r: 40, fill: 'red' })
|
||||
]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Special Props
|
||||
|
||||
| Prop | Behaviour |
|
||||
| :--- | :--- |
|
||||
| **`ref`** | `ref: (el) => ...` or `ref: { current: null }` – provides direct access to the DOM node after creation. |
|
||||
| **`onEvent`** | Any prop starting with `on` (e.g., `onClick`, `onInput`) is treated as an event listener. Automatically removed on cleanup. |
|
||||
| **`value` / `checked`** | When a signal is passed, creates two‑way binding for inputs, textareas, and selects. |
|
||||
| **`class`** | You can use `class` (not `className`). Accepts a string or a reactive function. |
|
||||
|
||||
---
|
||||
|
||||
## `h` vs Global Tag Helpers
|
||||
|
||||
| Feature | `h('div', ...)` | `div(...)` |
|
||||
| :--- | :--- | :--- |
|
||||
| **Dynamic tag names** | ✅ `h(tagName, ...)` | ❌ Must know tag name at write time |
|
||||
| **Explicit style** | More verbose | Cleaner, DSL‑like |
|
||||
| **Tree shaking** | Same | Same (helpers are generated once) |
|
||||
| **Performance** | Identical | Identical (helpers call `h` internally) |
|
||||
|
||||
> **Recommendation:** Use global tag helpers (`div()`, `button()`, etc.) for most cases – they are shorter and more readable. Use `h` directly only when the tag name is dynamic (e.g., `h(props.tag, ...)`).
|
||||
|
||||
---
|
||||
|
||||
## Complete Example
|
||||
|
||||
```javascript
|
||||
import { $, h, mount } from 'sigpro';
|
||||
|
||||
const dynamicTag = $('h1');
|
||||
|
||||
const App = () =>
|
||||
h('div', { class: 'demo' }, [
|
||||
h(dynamicTag(), {}, () => `Current tag: ${dynamicTag()}`),
|
||||
h('button', { onclick: () => dynamicTag(dynamicTag() === 'h1' ? 'h2' : 'h1') }, 'Toggle heading size')
|
||||
]);
|
||||
|
||||
mount(App, '#app');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
- `h` is the low‑level DOM builder used internally by all tag helpers.
|
||||
- It supports reactive attributes, reactive children, two‑way binding, event listeners, and SVG.
|
||||
- Use `h` directly when you need a **dynamic tag name**; otherwise, prefer the convenient global helpers.
|
||||
- Components written with `h` are fully reactive and automatically cleaned up.
|
||||
104
docs/api/html.md
104
docs/api/html.md
@@ -1,104 +0,0 @@
|
||||
# The DOM Factory: `Tag( )`
|
||||
|
||||
`Tag` is the internal engine that creates, attributes, and attaches reactivity to DOM elements. It uses `Watch` to maintain a live, high-performance link between your Signals and the Document Object Model.
|
||||
|
||||
## Function Signature
|
||||
|
||||
```typescript
|
||||
Tag(tagName: string, props?: Object, children?: any[] | any): HTMLElement
|
||||
```
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **`tagName`** | `string` | Yes | Valid HTML tag name (e.g., `"div"`, `"button"`). |
|
||||
| **`props`** | `Object` | No | Attributes, Events, Two-way bindings, and **Refs**. |
|
||||
| **`children`** | `any` | No | Nested elements, text strings, or reactive functions. |
|
||||
|
||||
---
|
||||
|
||||
## Key Features
|
||||
|
||||
### 1. Manual DOM Access: `ref`
|
||||
The `ref` property allows you to capture the underlying `HTMLElement` as soon as it is created. This is essential for integrating 3rd-party libraries (like AG Grid or Chart.js) or managing focus and measurements.
|
||||
|
||||
* **Callback Ref**: A function that receives the `HTMLElement` immediately.
|
||||
* **Object Ref**: An object with a `.current` property assigned to the `HTMLElement`.
|
||||
|
||||
```javascript
|
||||
// Auto-focus on mount
|
||||
Input({ ref: (el) => el.focus() });
|
||||
|
||||
// Capturing a node for an external library
|
||||
const gridDiv = { current: null };
|
||||
Div({ ref: gridDiv, class: "ag-theme-quartz" });
|
||||
```
|
||||
|
||||
### 2. Attribute Handling
|
||||
SigPro intelligently decides how to apply each property:
|
||||
* **Standard Props**: Applied via `setAttribute` or direct property assignment.
|
||||
* **Class Names**: Supports `class` or `className` interchangeably.
|
||||
* **Boolean Props**: Automatic handling for `checked`, `disabled`, `hidden`, etc.
|
||||
* **Note**: The `ref` property is intercepted and **never** rendered as an attribute in the HTML.
|
||||
|
||||
### 3. Event Listeners
|
||||
Events are defined by the `on` prefix. SigPro automatically registers the listener and ensures it is cleaned up when the element is destroyed.
|
||||
|
||||
```javascript
|
||||
Button({
|
||||
onclick: (e) => console.log("Clicked!", e),
|
||||
}, "Click Me");
|
||||
```
|
||||
|
||||
### 4. Reactive Attributes (One-Way)
|
||||
If an attribute value is a **function** (like a Signal), `Tag` creates an internal **`Watch`** to keep the DOM in sync with the state.
|
||||
|
||||
```javascript
|
||||
Div({
|
||||
// Updates the class whenever 'theme()' changes
|
||||
class: () => theme() === "dark" ? "bg-black" : "bg-white"
|
||||
});
|
||||
```
|
||||
|
||||
### 5. Smart Two-Way Binding (Automatic)
|
||||
SigPro automatically enables **bidirectional synchronization** when it detects a **Signal** assigned to a form-capable attribute (`value` or `checked`) on an input element (`input`, `textarea`, `select`).
|
||||
|
||||
```javascript
|
||||
// Syncs input value <-> signal automatically
|
||||
Input({
|
||||
type: "text",
|
||||
value: username // No special symbols needed!
|
||||
})
|
||||
```
|
||||
> **Note:** To use a Signal as **read-only** in an input, wrap it in an anonymous function: `value: () => username()`.
|
||||
|
||||
### 6. Reactive Children
|
||||
Children can be static or dynamic. When a child is a function, SigPro creates a reactive boundary using `Watch` for that specific part of the DOM.
|
||||
|
||||
```javascript
|
||||
Div({}, [
|
||||
H1("Static Title"),
|
||||
// Only this text node re-renders when 'count' changes
|
||||
() => `Current count: ${count()}`
|
||||
]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Memory Management (Internal)
|
||||
Every element created with `Tag` is "self-aware" regarding its reactive dependencies.
|
||||
* **`._cleanups`**: A hidden `Set` attached to the element that stores all `stop()` functions from its internal `Watch` calls and event listeners.
|
||||
* **Lifecycle**: When an element is removed by a Controller (`If`, `For`, or `Router`), SigPro performs a recursive **"sweep"** to execute these cleanups, ensuring **zero memory leaks**.
|
||||
|
||||
---
|
||||
|
||||
## Tag Constructors (The Shortcuts)
|
||||
|
||||
Instead of writing `Tag("div", ...)` every time, SigPro provides PascalCase global functions for all standard HTML tags. These are direct mappings to the `Tag` factory.
|
||||
|
||||
```javascript
|
||||
// This:
|
||||
Div({ class: "wrapper" }, [ Span("Hello") ])
|
||||
|
||||
// Is exactly equivalent to:
|
||||
Tag("div", { class: "wrapper" }, [ Tag("span", {}, "Hello") ])
|
||||
```
|
||||
180
docs/api/if.md
180
docs/api/if.md
@@ -1,180 +0,0 @@
|
||||
# Reactive Branching: `If( )`
|
||||
|
||||
The `If` function is a reactive control flow operator. It manages the conditional rendering of components with optional smooth transitions, ensuring that only the active branch exists in the DOM and in memory.
|
||||
|
||||
## Function Signature
|
||||
|
||||
```typescript
|
||||
If(
|
||||
condition: Signal<boolean> | Function,
|
||||
thenVal: Component | Node,
|
||||
otherwiseVal?: Component | Node,
|
||||
transition?: Transition
|
||||
): HTMLElement
|
||||
```
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **`condition`** | `Signal` | Yes | A reactive source that determines which branch to render. |
|
||||
| **`thenVal`** | `any` | Yes | The content to show when the condition is **truthy**. |
|
||||
| **`otherwiseVal`** | `any` | No | The content to show when the condition is **falsy** (defaults to null). |
|
||||
| **`transition`** | `Transition` | No | Optional animation hooks for enter/exit transitions. |
|
||||
|
||||
**Returns:** A `div` element with `display: contents` that acts as a reactive portal for the branches.
|
||||
|
||||
---
|
||||
|
||||
## Transition Interface
|
||||
|
||||
```typescript
|
||||
interface Transition {
|
||||
/** Called when branch enters. Use for fade-in, slide-in, etc. */
|
||||
in: (el: HTMLElement) => void;
|
||||
/** Called when branch leaves. Call `done()` when animation completes. */
|
||||
out: (el: HTMLElement, done: () => void) => void;
|
||||
}
|
||||
```
|
||||
|
||||
### Example: Fade Transition
|
||||
|
||||
```javascript
|
||||
const fade = {
|
||||
in: (el) => {
|
||||
el.style.opacity = "0";
|
||||
el.style.transition = "opacity 0.3s";
|
||||
requestAnimationFrame(() => {
|
||||
el.style.opacity = "1";
|
||||
});
|
||||
},
|
||||
out: (el, done) => {
|
||||
el.style.transition = "opacity 0.3s";
|
||||
el.style.opacity = "0";
|
||||
setTimeout(done, 300);
|
||||
}
|
||||
};
|
||||
|
||||
If(show, Modal, null, fade);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### 1. Simple Toggle
|
||||
|
||||
```javascript
|
||||
const isVisible = $(false);
|
||||
|
||||
Div([
|
||||
Button({ onclick: () => isVisible(!isVisible()) }, "Toggle Message"),
|
||||
|
||||
If(isVisible,
|
||||
P("Now you see me!"),
|
||||
P("Now you don't...")
|
||||
)
|
||||
]);
|
||||
```
|
||||
|
||||
### 2. With Smooth Animation
|
||||
|
||||
```javascript
|
||||
const showModal = $(false);
|
||||
|
||||
Div([
|
||||
Button({ onclick: () => showModal(true) }, "Open Modal"),
|
||||
|
||||
If(showModal,
|
||||
() => Modal({ onClose: () => showModal(false) }),
|
||||
null,
|
||||
fade // ← Smooth enter/exit animation
|
||||
)
|
||||
]);
|
||||
```
|
||||
|
||||
### 3. Lazy Component Loading
|
||||
|
||||
Unlike CSS `display: none`, `If` is **lazy**. The inactive branch is never created, saving memory.
|
||||
|
||||
```javascript
|
||||
If(() => user.isLogged(),
|
||||
() => Dashboard(), // Only executed if logged in
|
||||
() => LoginGate() // Only executed if guest
|
||||
)
|
||||
```
|
||||
|
||||
### 4. Complex Conditions
|
||||
|
||||
```javascript
|
||||
If(() => count() > 10 && status() === 'ready',
|
||||
Span("Threshold reached!")
|
||||
)
|
||||
```
|
||||
|
||||
### 5. If.not Helper
|
||||
|
||||
```javascript
|
||||
If.not(loading,
|
||||
() => Content(), // Shows when loading is FALSE
|
||||
() => Spinner() // Shows when loading is TRUE
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Automatic Cleanup
|
||||
|
||||
One of the core strengths of `If` is its integrated **Cleanup** logic. SigPro ensures that when a branch is swapped out, it is completely purged.
|
||||
|
||||
1. **Stop Watchers**: All `Watch` calls inside the inactive branch are permanently stopped.
|
||||
2. **Unbind Events**: Event listeners attached via `Tag` are removed.
|
||||
3. **Recursive Sweep**: SigPro performs a deep "sweep" of the removed branch.
|
||||
4. **Transition Respect**: When using transitions, destruction only happens AFTER the `out` animation completes.
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Function Wrappers**: For heavy components, use `() => MyComponent()` to prevent initialization until needed.
|
||||
- **Reusable Transitions**: Define common transitions (fade, slide, scale) in a shared module.
|
||||
- **Cleanup**: No manual cleanup needed. SigPro handles everything automatically.
|
||||
|
||||
---
|
||||
|
||||
## Technical Comparison
|
||||
|
||||
| Feature | Standard CSS `hidden` | SigPro `If` |
|
||||
| :--- | :--- | :--- |
|
||||
| **DOM Presence** | Always present | Only if active |
|
||||
| **Reactivity** | Still processing | **Paused/Destroyed** |
|
||||
| **Memory usage** | Higher | **Optimized** |
|
||||
| **Cleanup** | Manual | **Automatic** |
|
||||
| **Smooth Transitions** | Manual | **Built-in hook** |
|
||||
| **Animation Timing** | You manage | **Respected by core** |
|
||||
|
||||
---
|
||||
|
||||
## Complete Transition Examples
|
||||
|
||||
### Fade
|
||||
```javascript
|
||||
const fade = {
|
||||
in: (el) => { el.style.opacity = "0"; requestAnimationFrame(() => { el.style.transition = "opacity 0.3s"; el.style.opacity = "1"; }); },
|
||||
out: (el, done) => { el.style.transition = "opacity 0.3s"; el.style.opacity = "0"; setTimeout(done, 300); }
|
||||
};
|
||||
```
|
||||
|
||||
### Slide
|
||||
```javascript
|
||||
const slide = {
|
||||
in: (el) => { el.style.transform = "translateX(-100%)"; requestAnimationFrame(() => { el.style.transition = "transform 0.3s"; el.style.transform = "translateX(0)"; }); },
|
||||
out: (el, done) => { el.style.transition = "transform 0.3s"; el.style.transform = "translateX(-100%)"; setTimeout(done, 300); }
|
||||
};
|
||||
```
|
||||
|
||||
### Scale
|
||||
```javascript
|
||||
const scale = {
|
||||
in: (el) => { el.style.transform = "scale(0)"; requestAnimationFrame(() => { el.style.transition = "transform 0.2s"; el.style.transform = "scale(1)"; }); },
|
||||
out: (el, done) => { el.style.transition = "transform 0.2s"; el.style.transform = "scale(0)"; setTimeout(done, 200); }
|
||||
};
|
||||
```
|
||||
262
docs/api/jsx.md
262
docs/api/jsx.md
@@ -1,23 +1,174 @@
|
||||
# JSX with SigPro
|
||||
Aquí tienes el archivo `h.md` actualizado, que ahora incluye:
|
||||
|
||||
SigPro works seamlessly with JSX.
|
||||
- La función `h` (hyperscript)
|
||||
- Los helpers globales de etiquetas (lowercase)
|
||||
- Una nueva sección sobre **JSX con SigPro** (configuración, ejemplos y alternativas como `htm`).
|
||||
|
||||
## Configuration
|
||||
El documento está en inglés, como los originales, y listo para integrarse en tu documentación.
|
||||
|
||||
### TypeScript
|
||||
---
|
||||
|
||||
```markdown
|
||||
# Hyperscript & Tag Helpers
|
||||
|
||||
SigPro provides two complementary ways to create DOM elements:
|
||||
|
||||
1. **The `h` function** – the low‑level DOM builder.
|
||||
2. **Global Tag Helpers** (e.g., `div()`, `button()`, `span()`) – a convenient DSL built on top of `h`.
|
||||
|
||||
Both are reactive, auto‑cleanup, and support SVG, events, two‑way binding, and dynamic children.
|
||||
|
||||
---
|
||||
|
||||
## `h( )` – Hyperscript Function
|
||||
|
||||
The `h` function is the **core DOM builder** of SigPro. Use it directly when you need a dynamic tag name or prefer an explicit style.
|
||||
|
||||
### Function Signature
|
||||
|
||||
```typescript
|
||||
h(
|
||||
tag: string | Function,
|
||||
props?: object | Node | any[],
|
||||
children?: any
|
||||
): Node
|
||||
```
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| **`tag`** | `string` or `Function` | HTML tag name (e.g., `"div"`) or a component function. |
|
||||
| **`props`** | `object` | Optional. Attributes, event handlers, refs, etc. |
|
||||
| **`children`** | `any` | Optional. Text, nodes, arrays, or reactive functions. |
|
||||
|
||||
**Returns:** A DOM node (or array of nodes when the tag is a component that returns an array).
|
||||
|
||||
### Usage Examples
|
||||
|
||||
```js
|
||||
// Basic element
|
||||
h('div', {}, 'Hello world');
|
||||
|
||||
// With attributes and events
|
||||
h('button', { class: 'btn', onclick: () => alert('clicked') }, 'Click me');
|
||||
|
||||
// Nested children
|
||||
h('div', { class: 'container' }, [
|
||||
h('h1', {}, 'Title'),
|
||||
h('p', {}, 'Paragraph')
|
||||
]);
|
||||
|
||||
// Reactive child (function)
|
||||
const count = $(0);
|
||||
h('div', {}, [
|
||||
h('p', {}, () => `Count: ${count()}`),
|
||||
h('button', { onclick: () => count(count() + 1) }, '+1')
|
||||
]);
|
||||
|
||||
// Reactive attribute
|
||||
const theme = $('dark');
|
||||
h('div', { class: () => `box ${theme()}` }, 'Themed box');
|
||||
|
||||
// Two-way binding on input
|
||||
const name = $('');
|
||||
h('input', { type: 'text', value: name, placeholder: 'Your name' });
|
||||
h('p', {}, () => `Hello, ${name()}`);
|
||||
|
||||
// Component as tag
|
||||
const Button = (props, { children }) =>
|
||||
h('button', { class: 'btn', onclick: props.onClick }, children);
|
||||
|
||||
h(Button, { onClick: () => alert('clicked') }, 'Click me');
|
||||
|
||||
// SVG (auto-namespace)
|
||||
h('svg', { width: 100, height: 100 }, [
|
||||
h('circle', { cx: 50, cy: 50, r: 40, fill: 'red' })
|
||||
]);
|
||||
```
|
||||
|
||||
### Special Props
|
||||
|
||||
| Prop | Behaviour |
|
||||
| :--- | :--- |
|
||||
| **`ref`** | `ref: (el) => ...` or `ref: { current: null }` – direct DOM node access. |
|
||||
| **`onEvent`** | Any prop starting with `on` (e.g., `onClick`) is an event listener – auto‑removed on cleanup. |
|
||||
| **`value` / `checked`** | When a signal is passed, creates two‑way binding for form elements. |
|
||||
| **`class`** | Use `class` (not `className`). Accepts a string or reactive function. |
|
||||
|
||||
---
|
||||
|
||||
## Global Tag Helpers (Lowercase)
|
||||
|
||||
When you import SigPro (either via `import 'sigpro'` or the CDN), it automatically injects a helper function for **every standard HTML tag** directly onto `window`. These helpers are **lowercase** and work exactly like `h`, but with a cleaner syntax.
|
||||
|
||||
### Available Helpers
|
||||
|
||||
All standard HTML5 tags: `div`, `span`, `p`, `section`, `nav`, `header`, `footer`, `h1`…`h6`, `ul`, `ol`, `li`, `button`, `a`, `input`, `form`, `table`, `svg`, `circle`, etc.
|
||||
|
||||
### Usage Examples
|
||||
|
||||
```js
|
||||
// Instead of h('div', ...)
|
||||
div({ class: 'container' }, 'Content');
|
||||
|
||||
// Children only (skip props)
|
||||
section([
|
||||
h2('Title'),
|
||||
p('Paragraph')
|
||||
]);
|
||||
|
||||
// Reactive attribute
|
||||
const theme = $('light');
|
||||
div({ class: () => `app-${theme()}` }, 'Themed');
|
||||
|
||||
// Two-way binding
|
||||
const search = $('');
|
||||
input({ type: 'text', value: search, placeholder: 'Search...' });
|
||||
p(() => `You typed: ${search()}`);
|
||||
|
||||
// Dynamic children
|
||||
const count = $(0);
|
||||
div([
|
||||
p(() => `Count: ${count()}`),
|
||||
button({ onClick: () => count(count() + 1) }, '+1')
|
||||
]);
|
||||
```
|
||||
|
||||
### Complete Example
|
||||
|
||||
```js
|
||||
const App = () =>
|
||||
div({ class: 'app' }, [
|
||||
h1('Welcome'),
|
||||
input({ value: name, placeholder: 'Your name' }),
|
||||
p(() => `Hello, ${name() || 'stranger'}!`),
|
||||
button({ onClick: () => alert('Hi') }, 'Click me')
|
||||
]);
|
||||
|
||||
mount(App, '#app');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## JSX with SigPro
|
||||
|
||||
SigPro works seamlessly with JSX. You can use JSX as a compile‑time syntax sugar for `h` calls.
|
||||
|
||||
### Configuration
|
||||
|
||||
#### TypeScript
|
||||
|
||||
```json
|
||||
// tsconfig.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"jsxFactory": "Tag",
|
||||
"jsxFactory": "h",
|
||||
"jsxFragmentFactory": "Fragment"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Vite
|
||||
#### Vite
|
||||
|
||||
```js
|
||||
// vite.config.js
|
||||
@@ -25,31 +176,33 @@ import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
esbuild: {
|
||||
jsxFactory: 'Tag',
|
||||
jsxFactory: 'h',
|
||||
jsxFragmentFactory: 'Fragment'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Babel
|
||||
#### Babel
|
||||
|
||||
```js
|
||||
// babel.config.js
|
||||
export default {
|
||||
plugins: [
|
||||
['@babel/plugin-transform-react-jsx', {
|
||||
pragma: 'Tag',
|
||||
pragma: 'h',
|
||||
pragmaFrag: 'Fragment'
|
||||
}]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Example
|
||||
> **Note:** You need to import `h` and `Fragment` from SigPro in every JSX file, or make them global.
|
||||
|
||||
### JSX Example
|
||||
|
||||
```jsx
|
||||
// App.jsx
|
||||
import { $, Mount, Fragment } from 'sigpro';
|
||||
import { $, h, Fragment, mount } from 'sigpro';
|
||||
|
||||
const Button = ({ onClick, children }) => (
|
||||
<button class="btn btn-primary" onclick={onClick}>
|
||||
@@ -76,10 +229,10 @@ const App = () => {
|
||||
);
|
||||
};
|
||||
|
||||
Mount(App, '#app');
|
||||
mount(App, '#app');
|
||||
```
|
||||
|
||||
## What Gets Compiled
|
||||
### What Gets Compiled
|
||||
|
||||
Your JSX:
|
||||
```jsx
|
||||
@@ -89,59 +242,53 @@ Your JSX:
|
||||
```
|
||||
|
||||
Compiles to:
|
||||
```javascript
|
||||
Tag('div', { class: "container" },
|
||||
Tag(Button, {}, "Click")
|
||||
```js
|
||||
h('div', { class: "container" },
|
||||
h(Button, {}, "Click")
|
||||
)
|
||||
```
|
||||
|
||||
## Without Build Step (CDN)
|
||||
---
|
||||
|
||||
SigPro automatically injects `Div()`, `Button()`, `Span()`, and all other HTML tag helpers globally when loaded via CDN. `Fragment` is also available.
|
||||
## Without a Build Step (CDN + Tag Helpers)
|
||||
|
||||
If you don’t want to configure a JSX compiler, you can use the global tag helpers directly. They are available after loading SigPro via CDN.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://unpkg.com/sigpro"></script>
|
||||
<script type="module">
|
||||
import 'https://cdn.jsdelivr.net/npm/sigpro@1.2.18/+esm';
|
||||
// Now $, $$, watch, h, mount, div, button, etc. are global
|
||||
|
||||
const count = $(0);
|
||||
const App = () =>
|
||||
div({ class: 'container' }, [
|
||||
h1(() => `Count: ${count()}`),
|
||||
button({ onClick: () => count(count() + 1) }, 'Increment')
|
||||
]);
|
||||
|
||||
mount(App, '#app');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<script>
|
||||
const { $, Mount, Fragment } = SigPro;
|
||||
|
||||
const App = () => {
|
||||
const count = $(0);
|
||||
|
||||
return Div({ class: "container p-8" }, [
|
||||
H1({ class: "text-2xl font-bold" }, "SigPro Demo"),
|
||||
Button({
|
||||
class: "btn-primary",
|
||||
onclick: () => count(count() + 1)
|
||||
}, () => `Clicks: ${count()}`),
|
||||
Fragment({}, [
|
||||
P({}, "Multiple elements"),
|
||||
P({}, "Without wrapper")
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
Mount(App, '#app');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## Template Literals Alternative (htm)
|
||||
---
|
||||
|
||||
For a JSX-like syntax without a build step, use `htm`:
|
||||
## Template Literals Alternative (`htm`)
|
||||
|
||||
```javascript
|
||||
import { $, Mount } from 'https://unpkg.com/sigpro';
|
||||
For a JSX‑like syntax without a build step, you can combine SigPro with [`htm`](https://github.com/developit/htm).
|
||||
|
||||
```js
|
||||
import { $, h, mount } from 'https://cdn.jsdelivr.net/npm/sigpro@1.2.18/+esm';
|
||||
import htm from 'https://esm.sh/htm';
|
||||
|
||||
const html = htm.bind(Tag);
|
||||
const html = htm.bind(h); // bind to SigPro's h function
|
||||
|
||||
const App = () => {
|
||||
const count = $(0);
|
||||
@@ -156,16 +303,21 @@ const App = () => {
|
||||
`;
|
||||
};
|
||||
|
||||
Mount(App, '#app');
|
||||
mount(App, '#app');
|
||||
```
|
||||
|
||||
## Summary
|
||||
---
|
||||
|
||||
| Method | Build Step | Syntax |
|
||||
|--------|------------|--------|
|
||||
| JSX | Required | `<div>...</div>` |
|
||||
| CDN (Tag Helpers) | Optional | `Div({}, ...)` |
|
||||
| htm | Optional | `` html`<div>...</div>` `` |
|
||||
## Summary Comparison
|
||||
|
||||
> [!TIP]
|
||||
> **Recommendation:** Use JSX for large projects, CDN tag helpers (`Div()`, `Button()`) for simple projects, or htm for buildless projects that want HTML-like syntax.
|
||||
| Method | Build Step | Syntax | Recommended for |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **`h` function** | Optional | `h('div', ...)` | Dynamic tag names, low‑level control |
|
||||
| **Tag Helpers** | Optional | `div(...)` | Most cases – clean, simple, no build step |
|
||||
| **JSX** | Required | `<div>...</div>` | Large projects, teams familiar with React syntax |
|
||||
| **`htm`** | Optional | `` html`<div>...</div>` `` | Buildless but HTML‑like syntax |
|
||||
|
||||
> **Tip:** All approaches are fully reactive, support two‑way binding, events, SVG, and automatic cleanup. Choose the one that fits your workflow.
|
||||
```
|
||||
|
||||
```
|
||||
@@ -1,85 +1,148 @@
|
||||
# Application Mounter: `Mount( )`
|
||||
# Application Mounter: `mount( )`
|
||||
|
||||
The `Mount` function is the entry point of your reactive world. It bridges the gap between your SigPro logic and the browser's Real DOM by injecting a component into the document and initializing its reactive lifecycle.
|
||||
The `mount` function is the entry point of your reactive world. It bridges the gap between your SigPro logic and the browser's real DOM by rendering a component into a target element and managing its full reactive lifecycle.
|
||||
|
||||
## Function Signature
|
||||
|
||||
```typescript
|
||||
Mount(node: Function | HTMLElement, target?: string | HTMLElement): RuntimeObject
|
||||
mount(component: Function | Node, target: string | HTMLElement): RuntimeObject
|
||||
```
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
| Parameter | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **`node`** | `Function` or `Node` | **Required** | The component function or DOM element to render. |
|
||||
| **`target`** | `string` or `Node` | `document.body` | CSS selector or DOM element where the app will live. |
|
||||
| **`component`** | `Function` or `Node` | Yes | A component function (returns a Node) or a direct DOM node. |
|
||||
| **`target`** | `string` or `HTMLElement` | Yes | CSS selector (e.g., `"#app"`) or DOM element where the app will be mounted. |
|
||||
|
||||
**Returns:** A `Runtime` object containing the `container` and a `destroy()` method to wipe all reactivity and DOM nodes.
|
||||
**Returns:** A `Runtime` object with:
|
||||
- `container`: The actual DOM element created by the renderer.
|
||||
- `destroy()`: A method to completely unmount and clean up the application.
|
||||
|
||||
---
|
||||
|
||||
## Common Usage Scenarios
|
||||
## Usage Patterns
|
||||
|
||||
### 1. The SPA Entry Point
|
||||
In a Single Page Application, you typically mount your main component to the body or a root div. SigPro manages the entire view from that point.
|
||||
### 1. Main Application Entry Point
|
||||
|
||||
```javascript
|
||||
import SigPro from 'sigpro';
|
||||
import App from './App.js';
|
||||
import { mount } from 'sigpro';
|
||||
|
||||
// Mounts your main App component
|
||||
Mount(App, '#app-root');
|
||||
const App = () => div({ class: "app" }, [
|
||||
h1("Hello SigPro"),
|
||||
button("Click me")
|
||||
]);
|
||||
|
||||
mount(App, '#app');
|
||||
```
|
||||
|
||||
### 2. Reactive "Islands"
|
||||
SigPro is perfect for adding reactivity to static pages. You can mount small widgets into specific parts of an existing HTML layout.
|
||||
### 2. Reactive Widget (Island Architecture)
|
||||
|
||||
Mount small reactive components into static HTML pages.
|
||||
|
||||
```javascript
|
||||
const Counter = () => {
|
||||
const count = $(0);
|
||||
return Button({ onclick: () => count(c => c + 1) }, [
|
||||
"Clicks: ", count
|
||||
]);
|
||||
return button({ onclick: () => count(count() + 1) }, () => `Clicks: ${count()}`);
|
||||
};
|
||||
|
||||
// Mount only the counter into a specific sidebar div
|
||||
Mount(Counter, '#sidebar-widget');
|
||||
mount(Counter, '#sidebar-widget');
|
||||
```
|
||||
|
||||
### 3. Direct Node Mounting
|
||||
|
||||
You can also mount an already existing DOM node.
|
||||
|
||||
```javascript
|
||||
const myDiv = div("I am already a node");
|
||||
mount(myDiv, '#container');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How it Works (Lifecycle & Cleanup)
|
||||
## How It Works (Lifecycle & Cleanup)
|
||||
|
||||
When `Mount` is executed, it performs these critical steps to ensure a leak-free environment:
|
||||
When you call `mount`, SigPro performs these steps:
|
||||
|
||||
1. **Duplicate Detection**: If you call `Mount` on a target that already has a SigPro instance, it automatically calls `.destroy()` on the previous instance. This prevents "Zombie Effects" from stacking in memory.
|
||||
2. **Internal Scoping**: It executes the component function inside an internal **Reactive Owner**. This captures every `Watch` and event listener created during the render.
|
||||
3. **Target Injection**: It clears the target using `replaceChildren()` and appends the new component.
|
||||
4. **Runtime Creation**: It returns a control object:
|
||||
* `container`: The actual DOM element created.
|
||||
* `destroy()`: The "kill switch" that runs all cleanups, stops all watchers, and removes the element from the DOM.
|
||||
1. **Duplicate Detection**
|
||||
SigPro keeps a `WeakMap` (`MOUNTED_NODES`) that tracks which DOM target already has a mounted runtime. If you mount a new component to the same target, the previous instance is **automatically destroyed** before the new one is rendered. This prevents memory leaks and “zombie effects”.
|
||||
|
||||
2. **Render Phase**
|
||||
The `render` function creates a **cleanup container** (a `div` with `style="display: contents"`), and executes the component inside a fresh reactive owner. All effects (`watch`), event listeners, and child components created during this render are captured.
|
||||
|
||||
3. **DOM Injection**
|
||||
The target element is cleared using `replaceChildren()`, and the container (which holds the rendered content) is appended.
|
||||
|
||||
4. **Runtime Object**
|
||||
Returns an object `{ _isRuntime: true, container, destroy }`. The `destroy` function recursively disposes all effects, cleans up DOM nodes, and removes the container from the parent.
|
||||
|
||||
---
|
||||
|
||||
## Manual Unmounting
|
||||
|
||||
While SigPro handles most cleanups automatically (via `If`, `For`, and `Router`), you can manually destroy any mounted instance. This is vital for imperatively managed UI like **Toasts** or **Modals**.
|
||||
You can call `destroy()` at any time to tear down the application. This is essential for imperatively managed UI like **modals**, **toasts**, or **dynamic panels**.
|
||||
|
||||
```javascript
|
||||
const instance = Mount(MyToast, '#toast-container');
|
||||
const widget = mount(MyToast, '#toast-container');
|
||||
|
||||
// Later, to remove the toast and kill its reactivity:
|
||||
instance.destroy();
|
||||
// Later, remove it completely:
|
||||
widget.destroy();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Automatic Re‑mount on Same Target
|
||||
|
||||
If you call `mount` a second time on the same target, SigPro automatically destroys the previous instance and replaces it with the new one. No manual cleanup required.
|
||||
|
||||
```javascript
|
||||
mount(LoginScreen, '#app');
|
||||
// ... later, after login
|
||||
mount(Dashboard, '#app'); // LoginScreen is destroyed automatically
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What is Automatically Cleaned Up
|
||||
|
||||
When `destroy()` is called (or when a new mount replaces an old one), everything is purged:
|
||||
|
||||
- All `watch` effects
|
||||
- All event listeners added via SigPro (`onClick`, `onInput`, etc.)
|
||||
- All child components created with `when`, `each`, or nested `mount` calls
|
||||
- Any custom cleanups registered with `onUnmount`
|
||||
|
||||
> **You only need manual cleanup** for external resources not managed by SigPro (e.g., `setInterval`, third‑party libraries, WebSocket connections). Use `onUnmount` for that.
|
||||
|
||||
---
|
||||
|
||||
## Complete Example
|
||||
|
||||
```javascript
|
||||
import { $, mount, div, h1, button } from 'sigpro';
|
||||
|
||||
const App = () => {
|
||||
const count = $(0);
|
||||
return div({ class: "demo" }, [
|
||||
h1(() => `Count: ${count()}`),
|
||||
button({ onClick: () => count(count() + 1) }, "Increment")
|
||||
]);
|
||||
};
|
||||
|
||||
const runtime = mount(App, '#app');
|
||||
|
||||
// Destroy after 10 seconds
|
||||
setTimeout(() => runtime.destroy(), 10000);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary Cheat Sheet
|
||||
|
||||
| Goal | Code Pattern |
|
||||
| Goal | Code |
|
||||
| :--- | :--- |
|
||||
| **Mount to body** | `Mount(App)` |
|
||||
| **Mount to CSS Selector** | `Mount(App, '#root')` |
|
||||
| **Mount to DOM Node** | `Mount(App, myElement)` |
|
||||
| **Clean & Re-mount** | Calling `Mount` again on the same target |
|
||||
| **Total Cleanup** | `const app = Mount(App); app.destroy();` |
|
||||
| Mount to a CSS selector | `mount(App, '#root')` |
|
||||
| Mount to a DOM element | `mount(App, document.getElementById('root'))` |
|
||||
| Mount a static node | `mount(div("Hello"), '#target')` |
|
||||
| Manual destruction | `const app = mount(App, '#app'); app.destroy();` |
|
||||
| Auto‑replace on same target | Just call `mount` again – SigPro handles cleanup. |
|
||||
|
||||
> **Note:** The function name is `mount` (lowercase). It is exported from SigPro and also available globally after importing the library. The target must exist in the DOM at the time of mounting.
|
||||
@@ -1,238 +1,353 @@
|
||||
# ⚡ Quick API Reference
|
||||
# ⚡ SigPro 1.2.18 – Complete API Reference
|
||||
|
||||
SigPro is a high-performance micro-framework that updates the **Real DOM** surgically. No Virtual DOM, no unnecessary re-renders, and built-in **Cleanup** (memory cleanup).
|
||||
|
||||
<div class="text-center my-8">
|
||||
<div class="flex justify-center gap-2 flex-wrap mb-4">
|
||||
<span class="badge badge-primary badge-lg font-mono text-lg">$-$$</span>
|
||||
<span class="badge badge-secondary badge-lg font-mono text-lg">Watch</span>
|
||||
<span class="badge badge-accent badge-lg font-mono text-lg">Tag</span>
|
||||
<span class="badge badge-info badge-lg font-mono text-lg">If</span>
|
||||
<span class="badge badge-success badge-lg font-mono text-lg">For</span>
|
||||
<span class="badge badge-warning badge-lg font-mono text-lg">Router</span>
|
||||
<span class="badge badge-error badge-lg font-mono text-lg">Mount</span>
|
||||
</div>
|
||||
<h1 class="text-5xl font-black bg-gradient-to-r from-primary via-secondary to-accent bg-clip-text text-transparent">
|
||||
⚡ All the power! ⚡
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
## Core Functions
|
||||
|
||||
Explore the reactive building blocks of SigPro.
|
||||
|
||||
<div class="overflow-x-auto my-8 border border-base-300 rounded-xl shadow-sm">
|
||||
<table class="table table-zebra w-full">
|
||||
<thead class="bg-base-200 text-base-content">
|
||||
<tr>
|
||||
<th>Function</th>
|
||||
<th>Signature</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code class="text-primary font-bold">$(val, key?)</code></td>
|
||||
<td class="font-mono text-xs opacity-70">(any, string?) => Signal</td>
|
||||
<td>Creates a <b>Signal</b>. If <code>key</code> is provided, it persists in <code>localStorage</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code class="text-primary font-bold">$(fn)</code></td>
|
||||
<td class="font-mono text-xs opacity-70">(function) => Computed</td>
|
||||
<td>Creates a <b>Computed Signal</b> that auto-updates when dependencies change.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code class="text-primary font-bold">$$(obj)</code></td>
|
||||
<td class="font-mono text-xs opacity-70">(object) => Proxy</td>
|
||||
<td>Creates a <b>Deep Reactive Proxy</b>. Track nested property access automatically. No need for manual signals.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code class="text-secondary font-bold">Watch(fn)</code></td>
|
||||
<td class="font-mono text-xs opacity-70">(function) => stopFn</td>
|
||||
<td><b>Auto Mode:</b> Tracks any signal touched inside. Returns a stop function.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code class="text-secondary font-bold">Watch(deps, fn)</code></td>
|
||||
<td class="font-mono text-xs opacity-70">(Array, function) => stopFn</td>
|
||||
<td><b>Explicit Mode:</b> Only runs when signals in <code>deps</code> change.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code class="text-accent font-bold">If(cond, then, else?)</code></td>
|
||||
<td class="font-mono text-xs opacity-70">(Signal|bool, fn, fn?) => Node</td>
|
||||
<td>Reactive conditional. Automatically destroys "else" branch memory.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code class="text-accent font-bold">For(src, render, key)</code></td>
|
||||
<td class="font-mono text-xs opacity-70">(Signal, fn, fn) => Node</td>
|
||||
<td><b>Keyed Loop:</b> Optimized list renderer. Uses the key function for surgical DOM moves.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code class="text-info font-bold">Router(routes)</code></td>
|
||||
<td class="font-mono text-xs opacity-70">(Array) => Node</td>
|
||||
<td><b>SPA Router:</b> Hash-based routing with dynamic params (<code>:id</code>) and auto-cleanup.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code class="font-bold">Mount(node, target)</code></td>
|
||||
<td class="font-mono text-xs opacity-70">(any, string|Node) => Runtime</td>
|
||||
<td>Entry point. Cleans the target and mounts the app with full lifecycle management.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Element Constructors (Tags)
|
||||
|
||||
SigPro provides **PascalCase** wrappers for all standard HTML5 tags (e.g., `Div`, `Span`, `Button`).
|
||||
|
||||
### Syntax Pattern
|
||||
|
||||
<div class="mockup-code bg-base-300 text-base-content">
|
||||
<pre data-prefix=""><code>Tag({ attributes }, [children])</code></pre>
|
||||
</div>
|
||||
|
||||
### Special Attributes & Routing
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 my-10">
|
||||
<div class="card bg-base-200 border border-base-300 shadow-sm">
|
||||
<div class="card-body p-5">
|
||||
<h3 class="text-xs font-black uppercase tracking-widest opacity-60">Two-way Binding</h3>
|
||||
<code class="text-primary font-bold text-sm bg-base-300/50 p-2 rounded-lg">value: mySignal</code>
|
||||
<p class="text-xs mt-3 leading-relaxed">Automatic sync for <code>Input</code>, <code>Textarea</code>, and <code>Select</code>. Updates the signal on 'input' or 'change'.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-200 border border-base-300 shadow-sm">
|
||||
<div class="card-body p-5">
|
||||
<h3 class="text-xs font-black uppercase tracking-widest opacity-60">Dynamic Routing</h3>
|
||||
<code class="text-info font-bold text-sm bg-base-300/50 p-2 rounded-lg">Router.to('/user/1')</code>
|
||||
<p class="text-xs mt-3 leading-relaxed">Navigate programmatically. Access params via <code>Router.params().id</code>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-200 border border-base-300 shadow-sm">
|
||||
<div class="card-body p-5">
|
||||
<h3 class="text-xs font-black uppercase tracking-widest opacity-60">Refs & DOM</h3>
|
||||
<code class="text-accent font-bold text-sm bg-base-300/50 p-2 rounded-lg">ref: (el) => ...</code>
|
||||
<p class="text-xs mt-3 leading-relaxed">Get direct access to the DOM node once it is created.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-200 border border-base-300 shadow-sm">
|
||||
<div class="card-body p-5">
|
||||
<h3 class="text-xs font-black uppercase tracking-widest opacity-60">Event Handling</h3>
|
||||
<code class="text-secondary font-bold text-sm bg-base-300/50 p-2 rounded-lg">onClick: (e) => ...</code>
|
||||
<p class="text-xs mt-3 leading-relaxed">Standard events with automatic <code>removeEventListener</code> on destruction.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Custom API (Bring Your Own Syntax)
|
||||
|
||||
SigPro's core functions are intentionally simple and can be easily renamed in **one line** to match your preferred coding style.
|
||||
|
||||
### One-Line Renaming
|
||||
SigPro is a **Real‑DOM first** reactive micro‑framework. No virtual DOM, no diffing overhead – it updates the DOM directly with surgical precision. Built‑in automatic cleanup prevents memory leaks, and the API is designed to be both tiny and powerful.
|
||||
|
||||
```javascript
|
||||
import { $ as signal, Mount as render, Tag as tag, If as when, For as each, Watch as effect } from 'sigpro';
|
||||
import { $, $$, watch, h, when, each, fx, router, req, mount, batch } from 'sigpro'
|
||||
// or use globally as window.$ etc.
|
||||
```
|
||||
|
||||
// Now use your custom names
|
||||
const count = signal(0);
|
||||
effect(() => console.log(count()));
|
||||
---
|
||||
|
||||
render(() =>
|
||||
tag('div', {}, [
|
||||
when(count,
|
||||
() => tag('span', {}, 'Positive'),
|
||||
() => tag('span', {}, 'Zero or negative')
|
||||
## 🔁 Core Reactivity
|
||||
|
||||
### `$(value, localStorageKey?)` – Signal & Computed
|
||||
|
||||
Creates a reactive signal. If a function is passed, it becomes a **computed** signal that caches its result until dependencies change.
|
||||
|
||||
| Usage | Description |
|
||||
|-------|-------------|
|
||||
| `const count = $(0)` | Basic signal, returns a getter/setter: `count()` reads, `count(5)` writes. |
|
||||
| `const double = $( () => count() * 2 )` | Computed signal – updates automatically when `count` changes. |
|
||||
| `const stored = $('hello', 'myKey')` | Persisted signal – reads/writes to `localStorage`. |
|
||||
|
||||
**Example**
|
||||
```javascript
|
||||
const count = $(0)
|
||||
const double = $( () => count() * 2 )
|
||||
|
||||
watch(() => {
|
||||
console.log(`count = ${count()}, double = ${double()}`)
|
||||
}) // logs on every change
|
||||
|
||||
count(5) // triggers log: count=5, double=10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `$$(object)` – Deep Reactive Proxy
|
||||
|
||||
Makes a plain object (and all nested objects) deeply reactive. Any property access is tracked, any mutation triggers updates.
|
||||
|
||||
```javascript
|
||||
const state = $$({ user: { name: 'Alice', age: 30 }, items: [1,2,3] })
|
||||
|
||||
watch(() => {
|
||||
console.log(state.user.name) // tracks `user.name`
|
||||
})
|
||||
|
||||
state.user.name = 'Bob' // triggers the effect
|
||||
```
|
||||
|
||||
> **Note**: `$$` caches proxies per original object, so calling `$$` multiple times on the same object returns the same proxy.
|
||||
|
||||
---
|
||||
|
||||
### `watch(source, callback?)` – Reactive Effect
|
||||
|
||||
Two modes:
|
||||
|
||||
1. **Auto‑track mode** – pass a function: `watch(() => { /* reads signals */ })`
|
||||
Automatically re‑runs whenever any signal read inside changes.
|
||||
|
||||
2. **Explicit mode** – pass an array of signals and a callback:
|
||||
`watch([count, double], () => { ... })`
|
||||
Runs the callback when any of the listed signals change. The callback receives the new values.
|
||||
|
||||
Both modes return a `stop` function that disposes the effect.
|
||||
|
||||
```javascript
|
||||
// auto mode
|
||||
const stop = watch(() => console.log(count()))
|
||||
|
||||
// explicit mode
|
||||
watch([count, double], ([newCount, newDouble]) => {
|
||||
console.log(newCount, newDouble)
|
||||
})
|
||||
```
|
||||
|
||||
> **Important**: Effects are depth‑aware – they run in topological order, parents before children.
|
||||
|
||||
---
|
||||
|
||||
## 🧱 Components & DOM (Hyperscript)
|
||||
|
||||
### `h(tag, props, children)` – Create DOM Nodes
|
||||
|
||||
The universal builder. `props` can be omitted. Children can be strings, numbers, nodes, arrays, or **dynamic functions**.
|
||||
|
||||
| Feature | Example |
|
||||
|---------|---------|
|
||||
| Standard attributes | `h('div', { class: 'box', id: 'main' })` |
|
||||
| Events | `onClick: (e) => ...` (automatically cleaned up) |
|
||||
| Reactive attributes | `class: () => count() > 0 ? 'positive' : 'negative'` |
|
||||
| Two‑way binding | `value: mySignal` (works on `input`, `textarea`, `select`) |
|
||||
| Refs | `ref: (el) => ...` or `ref: { current: null }` |
|
||||
| SVG support | tag names like `svg`, `circle`, `path` – sets correct namespace |
|
||||
| Dangerous URL sanitising | `href` / `src` with `javascript:` or `data:` are blocked → `'#'` |
|
||||
|
||||
**Dynamic children** – pass a function as a child, it will be re‑executed and the DOM patched automatically:
|
||||
|
||||
```javascript
|
||||
h('div', {}, [
|
||||
() => count() > 0 ? h('span', {}, 'positive') : h('span', {}, 'zero or negative')
|
||||
])
|
||||
```
|
||||
|
||||
### Tag shortcuts
|
||||
|
||||
SigPro defines **all standard HTML5 tags** as PascalCase globals (when run in browser) and also exports them as named exports. Example:
|
||||
|
||||
```javascript
|
||||
Div({ class: 'container' }, [
|
||||
H1({}, 'Title'),
|
||||
Button({ onClick: () => alert('hi') }, 'Click me')
|
||||
])
|
||||
```
|
||||
|
||||
Available tags: `a`, `abbr`, `article`, `aside`, `audio`, `b`, `blockquote`, `br`, `button`, `canvas`, `caption`, `cite`, `code`, `col`, `colgroup`, `datalist`, `dd`, `del`, `details`, `dfn`, `dialog`, `div`, `dl`, `dt`, `em`, `embed`, `fieldset`, `figcaption`, `figure`, `footer`, `form`, `h1`…`h6`, `header`, `hr`, `i`, `iframe`, `img`, `input`, `ins`, `kbd`, `label`, `legend`, `li`, `main`, `mark`, `meter`, `nav`, `object`, `ol`, `optgroup`, `option`, `output`, `p`, `picture`, `pre`, `progress`, `section`, `select`, `slot`, `small`, `source`, `span`, `strong`, `sub`, `summary`, `sup`, `svg`, `table`, `tbody`, `td`, `template`, `textarea`, `tfoot`, `th`, `thead`, `time`, `tr`, `u`, `ul`, `video`.
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Flow Control Components
|
||||
|
||||
### `when(condition, thenComponent, elseComponent?)`
|
||||
|
||||
Reactive conditional rendering. `condition` can be a boolean, a signal, or any function that returns a boolean. Both branches can be `Node`, `() => Node`, or `null`. Automatically disposes the unmounted branch.
|
||||
|
||||
```javascript
|
||||
when(
|
||||
() => user.loggedIn(),
|
||||
() => Div({}, 'Welcome back!'),
|
||||
() => Button({ onClick: () => login() }, 'Login')
|
||||
)
|
||||
]),
|
||||
'#app'
|
||||
);
|
||||
```
|
||||
|
||||
### Create React-like Hooks
|
||||
---
|
||||
|
||||
### `each(source, itemRenderer, keyFn)`
|
||||
|
||||
Optimised keyed list rendering. `source` can be an array or a signal/function returning an array. `itemRenderer(item, index)` returns a Node (or a function that returns Nodes). `keyFn(item, index)` returns a unique identifier – **required** for efficient DOM reuse.
|
||||
|
||||
```javascript
|
||||
import * as SigPro from 'sigpro';
|
||||
const items = $([{ id: 1, text: 'a' }, { id: 2, text: 'b' }])
|
||||
|
||||
const useState = (initial) => {
|
||||
const signal = SigPro.$(initial);
|
||||
return [signal, (value) => signal(value)];
|
||||
};
|
||||
|
||||
const useEffect = (fn, deps) => {
|
||||
deps ? SigPro.Watch(deps, fn) : SigPro.Watch(fn);
|
||||
};
|
||||
|
||||
// Usage
|
||||
const Counter = () => {
|
||||
const [count, setCount] = useState(0);
|
||||
useEffect(() => console.log(count()), [count]);
|
||||
return SigPro.Tag('button', { onClick: () => setCount(count() + 1) }, count);
|
||||
};
|
||||
each(items,
|
||||
(item) => Li({}, item.text),
|
||||
(item) => item.id
|
||||
)
|
||||
```
|
||||
|
||||
### Create Vue-like API
|
||||
When the array changes, elements are added, removed, or reordered with minimal DOM operations.
|
||||
|
||||
---
|
||||
|
||||
## 💥 Effects & Lifecycle
|
||||
|
||||
### `onUnmount(fn)`
|
||||
|
||||
Inside a component (function called from `h`), registers a cleanup function that runs when that component is removed from the DOM.
|
||||
|
||||
```javascript
|
||||
import { $ as ref, Watch as watch, Mount as mount } from 'sigpro';
|
||||
|
||||
const computed = (fn) => ref(fn);
|
||||
const createApp = (component) => ({ mount: (selector) => mount(component, selector) });
|
||||
|
||||
// Usage
|
||||
const count = ref(0);
|
||||
const double = computed(() => count() * 2);
|
||||
watch([count], () => console.log(count()));
|
||||
const Timer = () => {
|
||||
const interval = setInterval(() => console.log('tick'), 1000)
|
||||
onUnmount(() => clearInterval(interval))
|
||||
return Div({}, 'Timer running')
|
||||
}
|
||||
```
|
||||
|
||||
### Global Custom API with sigpro.config.js
|
||||
### `batch(fn)`
|
||||
|
||||
Create a central configuration file to reuse your custom naming across the entire project:
|
||||
Batch multiple reactive updates into a single flush, improving performance.
|
||||
|
||||
```javascript
|
||||
// config/sigpro.config.js
|
||||
import { $ as signal, Mount as render, Tag as tag, If as when, For as each, Watch as effect } from 'sigpro';
|
||||
|
||||
// Re-export everything with your custom names
|
||||
export { signal, render, tag, when, each, effect };
|
||||
|
||||
// Also re-export the original functions if needed
|
||||
export * from 'sigpro';
|
||||
batch(() => {
|
||||
count(1)
|
||||
name('John')
|
||||
// effects run only once after the batch ends
|
||||
})
|
||||
```
|
||||
|
||||
### `untrack(fn)`
|
||||
|
||||
Run a function without tracking any signal reads.
|
||||
|
||||
```javascript
|
||||
// app.js - Import your custom API globally
|
||||
import { signal, render, tag, when, each, effect } from './config/sigpro.config.js';
|
||||
const logCount = () => {
|
||||
untrack(() => console.log('count is', count()))
|
||||
}
|
||||
```
|
||||
|
||||
// Use your preferred syntax everywhere
|
||||
const count = signal(0);
|
||||
const double = signal(() => count() * 2);
|
||||
---
|
||||
|
||||
effect(() => console.log(`Count: ${count()}, Double: ${double()}`));
|
||||
## ✨ Animations – `fx(options, child)`
|
||||
|
||||
Applies smooth enter animations (CSS transitions / keyframes). Returns the modified element.
|
||||
|
||||
```javascript
|
||||
fx({ name: 'fade', duration: 300 },
|
||||
Div({}, 'Hello')
|
||||
)
|
||||
```
|
||||
|
||||
**Options**
|
||||
- `name` – uses predefined keyframes `${name}-in` (you must define them in your CSS)
|
||||
- `duration` – in ms (default 200)
|
||||
- `scale` – adds `scale(0.95)` → `none`
|
||||
- `slide` – adds `translateY(-10px)` → `none`
|
||||
- `rotate` – adds `rotate(-2deg)` → `none`
|
||||
- `blur` – adds `blur(4px)` → `none`
|
||||
|
||||
If `name` is given, it sets `animation: ${name}-in ${duration}ms`. Otherwise it applies a smooth transition from the initial transform/filter to the final state.
|
||||
|
||||
---
|
||||
|
||||
## 🧭 Router – `router(routes)`
|
||||
|
||||
Hash‑based SPA router. Returns a DOM node that renders the current route.
|
||||
|
||||
```javascript
|
||||
const routes = [
|
||||
{ path: '/', component: () => Div({}, 'Home') },
|
||||
{ path: '/user/:id', component: (params) => Div({}, `User ${params.id}`) },
|
||||
{ path: '*', component: () => Div({}, '404') }
|
||||
]
|
||||
|
||||
const App = () => Div({}, [
|
||||
A({ href: '#/' }, 'Home'),
|
||||
A({ href: '#/user/42' }, 'User 42'),
|
||||
router(routes)
|
||||
])
|
||||
```
|
||||
|
||||
**API**
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `router.params()` | Returns a reactive signal of current route params (e.g., `{ id: '42' }`). |
|
||||
| `router.to(path)` | Navigate to a new hash (e.g., `router.to('/user/5')`). Prepend `#` automatically. |
|
||||
| `router.back()` | Go back in history. |
|
||||
| `router.path()` | Returns current hash path without `#` (e.g., `/user/42`). |
|
||||
|
||||
---
|
||||
|
||||
## 🌐 HTTP Requests – `req(config)`
|
||||
|
||||
Creates a reactive request controller with built‑in loading/error/data signals and abort support.
|
||||
|
||||
```javascript
|
||||
const fetchUser = req({ url: '/api/user/1', method: 'GET' })
|
||||
|
||||
// start the request
|
||||
fetchUser.run().catch(console.error)
|
||||
|
||||
// reactively display state
|
||||
watch(() => {
|
||||
if (fetchUser.loading()) console.log('loading...')
|
||||
if (fetchUser.error()) console.error(fetchUser.error())
|
||||
if (fetchUser.data()) console.log(fetchUser.data())
|
||||
})
|
||||
|
||||
// abort if needed
|
||||
fetchUser.abort()
|
||||
```
|
||||
|
||||
**Options**
|
||||
- `url` (required)
|
||||
- `method` (default `'GET'`)
|
||||
- `headers` (object, default `{}`)
|
||||
|
||||
**Return value**
|
||||
- `run(body?)` – initiates the request, returns a promise.
|
||||
- `abort()` – aborts the current request (AbortController).
|
||||
- `loading` – signal (boolean)
|
||||
- `error` – signal (`null` or error message)
|
||||
- `data` – signal (`null` or parsed JSON)
|
||||
|
||||
> **Note**: Automatically sets `Content-Type: application/json` unless `body` is a `FormData`. Timeout after 10 seconds aborts the request.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Mounting – `mount(component, target)`
|
||||
|
||||
Clears the target element and mounts the application. Returns the runtime instance (which has a `.destroy()` method).
|
||||
|
||||
```javascript
|
||||
mount(() => App(), '#app')
|
||||
// or
|
||||
mount(App, document.body)
|
||||
```
|
||||
|
||||
If you mount again on the same target, the previous instance is automatically destroyed.
|
||||
|
||||
---
|
||||
|
||||
## 🧹 Global Cleanup & Memory
|
||||
|
||||
SigPro tracks every effect, DOM event listener, and nested component. When a component is unmounted:
|
||||
- All its effects are disposed.
|
||||
- All DOM event listeners are removed.
|
||||
- All `onUnmount` callbacks run.
|
||||
- Child components are recursively destroyed.
|
||||
|
||||
You never need to manually clean up – just write reactive code.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Full Example – Counter with Persistence
|
||||
|
||||
```javascript
|
||||
import { $, watch, h, mount } from 'sigpro'
|
||||
|
||||
const count = $(0, 'counter') // persists in localStorage
|
||||
|
||||
const App = () =>
|
||||
tag('div', { class: 'p-4' }, [
|
||||
tag('h1', {}, () => `Count: ${count()}`),
|
||||
tag('button', { onclick: () => count(count() + 1) }, 'Increment')
|
||||
]);
|
||||
Div({ class: 'counter' }, [
|
||||
H1({}, () => `Count: ${count()}`),
|
||||
Button({ onClick: () => count(count() + 1) }, '+'),
|
||||
Button({ onClick: () => count(count() - 1) }, '-'),
|
||||
Button({ onClick: () => count(0) }, 'Reset')
|
||||
])
|
||||
|
||||
render(App, '#app');
|
||||
mount(App, '#app')
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> **Why rename?** Team preferences, framework migration, or just personal taste. SigPro adapts to you, not the other way around.
|
||||
---
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Performance Hint:** For lists (`For`), always provide a unique key function `(item) => item.id` to prevent unnecessary node creation and enable reordering.
|
||||
## 🔧 Customising the API (Renaming)
|
||||
|
||||
> [!TIP]
|
||||
> **Pro Tip:** Use `$$()` for complex nested state objects instead of multiple `$()` signals. It's cleaner and automatically tracks deep properties.
|
||||
You can rename everything in one line:
|
||||
|
||||
> [!TIP]
|
||||
> **Performance Hint:** Always use functions `() => signal()` for dynamic children to ensure SigPro only updates the specific node and not the whole container.
|
||||
```javascript
|
||||
import { $ as signal, watch as effect, h as element, mount as render } from 'sigpro'
|
||||
```
|
||||
|
||||
Or assign globally:
|
||||
|
||||
```javascript
|
||||
window.myReactive = $
|
||||
```
|
||||
|
||||
All functions are also exposed on the global `window` object when included via `<script>`.
|
||||
|
||||
---
|
||||
|
||||
## 📜 License & Version
|
||||
|
||||
Current version: **1.2.18**
|
||||
Released under MIT.
|
||||
Zero dependencies, ~3KB gzipped.
|
||||
|
||||
---
|
||||
|
||||
> **Need legacy IE support?** Not supported – requires modern JavaScript (Proxy, WeakMap, etc.).
|
||||
182
docs/api/req.md
Normal file
182
docs/api/req.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# HTTP Requests: `req( )`
|
||||
|
||||
The `req` function creates a **reactive HTTP request controller**. It returns signals for `loading`, `error`, and `data`, plus a `run` method to execute the request and an `abort` method to cancel it. All signals update automatically as the request progresses.
|
||||
|
||||
## Function Signature
|
||||
|
||||
```typescript
|
||||
req(config: {
|
||||
url: string;
|
||||
method?: string; // default: 'GET'
|
||||
headers?: Record<string, string>;
|
||||
}): {
|
||||
run: (body?: any) => Promise<any>;
|
||||
abort: () => void;
|
||||
loading: Signal<boolean>;
|
||||
error: Signal<string | null>;
|
||||
data: Signal<any | null>;
|
||||
}
|
||||
```
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **`url`** | `string` | Yes | The endpoint to call. |
|
||||
| **`method`** | `string` | No | HTTP method (`'GET'`, `'POST'`, etc.). Default `'GET'`. |
|
||||
| **`headers`** | `object` | No | Custom headers (will be merged with defaults). |
|
||||
|
||||
**Returns:** A controller object with reactive signals and methods.
|
||||
|
||||
---
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### 1. Basic GET Request
|
||||
|
||||
```javascript
|
||||
const users = req({ url: '/api/users' });
|
||||
|
||||
// Start the request
|
||||
users.run().catch(console.error);
|
||||
|
||||
// React to the response
|
||||
watch(() => {
|
||||
if (users.loading()) console.log('Loading...');
|
||||
if (users.error()) console.error(users.error());
|
||||
if (users.data()) console.log('Data:', users.data());
|
||||
});
|
||||
```
|
||||
|
||||
### 2. POST Request with Body
|
||||
|
||||
```javascript
|
||||
const createUser = req({ url: '/api/users', method: 'POST' });
|
||||
|
||||
const handleSubmit = async (formData) => {
|
||||
try {
|
||||
await createUser.run(formData);
|
||||
alert('User created!');
|
||||
} catch (err) {
|
||||
// Error already in createUser.error()
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Aborting a Request
|
||||
|
||||
```javascript
|
||||
const search = req({ url: '/api/search' });
|
||||
|
||||
// Abort if the user types again quickly
|
||||
let timeout;
|
||||
input({ onInput: (e) => {
|
||||
clearTimeout(timeout);
|
||||
search.abort(); // cancel previous in-flight request
|
||||
timeout = setTimeout(() => search.run({ q: e.target.value }), 300);
|
||||
}});
|
||||
```
|
||||
|
||||
### 4. Reactive UI with Signals
|
||||
|
||||
```javascript
|
||||
const profile = req({ url: '/api/me' });
|
||||
|
||||
const App = () =>
|
||||
div([
|
||||
when(() => profile.loading(),
|
||||
() => div("Loading...")
|
||||
),
|
||||
when(() => profile.error(),
|
||||
() => div("Error: " + profile.error())
|
||||
),
|
||||
when(() => profile.data(),
|
||||
() => div([
|
||||
h2(profile.data().name),
|
||||
p(profile.data().email)
|
||||
])
|
||||
)
|
||||
]);
|
||||
|
||||
profile.run();
|
||||
mount(App, '#app');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Request Lifecycle
|
||||
|
||||
When you call `run(body?)`:
|
||||
|
||||
1. Any previous request is **aborted** automatically.
|
||||
2. `loading` becomes `true`.
|
||||
3. `error` is cleared.
|
||||
4. A 10‑second timeout is set (auto‑abort).
|
||||
5. The request is sent using `fetch`.
|
||||
6. On success: `data` is set with the parsed JSON, `loading` becomes `false`.
|
||||
7. On error: `error` is set with the message, `data` is cleared, `loading` becomes `false`.
|
||||
|
||||
> **Important:** The response body is parsed as JSON. If the response is not OK or the JSON parsing fails, `error` is set and the promise rejects.
|
||||
|
||||
---
|
||||
|
||||
## Automatic Headers
|
||||
|
||||
- If `body` is a plain object or array, the request automatically includes `Content-Type: application/json` (unless you override it in `headers`).
|
||||
- If `body` is a `FormData` instance, the `Content-Type` is not set (browser will set it automatically with the correct boundary).
|
||||
- Other headers can be added via the `headers` option.
|
||||
|
||||
```javascript
|
||||
const upload = req({
|
||||
url: '/upload',
|
||||
method: 'POST',
|
||||
headers: { 'X-Custom': 'value' }
|
||||
});
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', fileInput.files[0]);
|
||||
upload.run(formData);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Network errors, timeouts, and HTTP error statuses (4xx, 5xx) all set `error` and cause the promise to reject.
|
||||
- The `error` signal contains a human‑readable message.
|
||||
- Abort errors (calling `abort()` or timeout) are **silent** – they do not set `error` or reject the promise?
|
||||
Actually, according to the source: if `e.name === 'AbortError'`, it does **not** call `error(e.message)`, but the promise still rejects with the AbortError. You can handle it with `.catch()` if needed.
|
||||
|
||||
---
|
||||
|
||||
## Complete Example
|
||||
|
||||
```javascript
|
||||
const api = req({ url: 'https://jsonplaceholder.typicode.com/posts/1' });
|
||||
|
||||
const App = () =>
|
||||
div({ class: "demo" }, [
|
||||
when(() => api.loading(), () => p("⏳ Loading...")),
|
||||
when(() => api.error(), () => p("❌ " + api.error())),
|
||||
when(() => api.data(), () => div([
|
||||
h2(api.data().title),
|
||||
p(api.data().body)
|
||||
])),
|
||||
button({
|
||||
onClick: () => api.run(),
|
||||
disabled: () => api.loading()
|
||||
}, "Fetch")
|
||||
]);
|
||||
|
||||
mount(App, '#app');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Member | Type | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| `run(body?)` | `(any) => Promise` | Starts the request. Returns a promise. |
|
||||
| `abort()` | `() => void` | Cancels the current request. |
|
||||
| `loading` | `Signal<boolean>` | `true` while request is in flight. |
|
||||
| `error` | `Signal<string\|null>` | Contains an error message, or `null`. |
|
||||
| `data` | `Signal<any\|null>` | Contains the parsed response (JSON), or `null`. |
|
||||
@@ -1,96 +1,187 @@
|
||||
# Routing: `Router()` & Utilities
|
||||
# Routing: `router( )` & Utilities
|
||||
|
||||
SigPro includes a built-in, lightweight **Hash Router** to create Single Page Applications (SPA). It manages the URL state, matches components to paths, and handles the lifecycle of your pages automatically.
|
||||
SigPro includes a built‑in, lightweight **hash router** to create single‑page applications (SPA). It manages the URL hash, matches components to routes with dynamic segments (`:id`), and automatically cleans up each page when you navigate away.
|
||||
|
||||
## Router Signature
|
||||
## Function Signature
|
||||
|
||||
```typescript
|
||||
Router(routes: Route[]): HTMLElement
|
||||
router(routes: Route[]): HTMLElement
|
||||
```
|
||||
|
||||
### Route Object
|
||||
|
||||
| Property | Type | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| **`path`** | `string` | The URL fragment (e.g., `"/"`, `"/user/:id"`, or `"*"`). |
|
||||
| **`component`** | `Function` | A function that returns a Node, a String, or a reactive View. |
|
||||
| **`path`** | `string` | The URL fragment pattern (e.g. `"/"`, `"/user/:id"`, or `"*"` for catch‑all). |
|
||||
| **`component`** | `Function` | A function that returns a Node, a string, or a reactive view. Receives `params` object as argument. |
|
||||
|
||||
**Returns:** A `div` element (with class `"router-hook"`) that acts as the router outlet. The router automatically destroys the previous view and mounts the matched component when the hash changes.
|
||||
|
||||
---
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### 1. Defining Routes
|
||||
The `Router` returns a `div` element with the class `.router-transition`. When the hash changes, the router destroys the previous view and mounts the new one inside this container.
|
||||
|
||||
Place the `router` element where you want the page content to appear. Inside the routes array, define your routes.
|
||||
|
||||
```javascript
|
||||
const App = () => Div({ class: "app-layout" }, [
|
||||
Navbar(),
|
||||
// The router outlet is placed here
|
||||
Router([
|
||||
const Home = () => h1("Home Page");
|
||||
const UserProfile = (params) => h1(`User ID: ${params.id}`);
|
||||
const NotFound = () => h1("404 – Page not found");
|
||||
|
||||
const App = () =>
|
||||
div({ class: "app-layout" }, [
|
||||
nav([
|
||||
a({ href: "#/" }, "Home"),
|
||||
a({ href: "#/user/42" }, "User 42")
|
||||
]),
|
||||
router([
|
||||
{ path: "/", component: Home },
|
||||
{ path: "/profile/:id", component: (params) => UserProfile(params.id) },
|
||||
{ path: "*", component: () => H1("404 Not Found") }
|
||||
{ path: "/user/:id", component: UserProfile },
|
||||
{ path: "*", component: NotFound }
|
||||
])
|
||||
]);
|
||||
|
||||
mount(App, "#app");
|
||||
```
|
||||
|
||||
### 2. Dynamic Segments (`:id`)
|
||||
The router automatically parses URL parameters (like `:id`) and passes them as an object to the component function. You can also access them globally via `Router.params()`.
|
||||
|
||||
When a route contains a colon‑prefixed segment (like `:id`), the router extracts the corresponding value from the current hash and passes it as a property inside the `params` object to the component function.
|
||||
|
||||
```javascript
|
||||
// If the URL is #/profile/42
|
||||
// If the hash is #/user/42
|
||||
const UserProfile = (params) => {
|
||||
return H1(`User ID is: ${params.id}`); // Displays "User ID is: 42"
|
||||
console.log(params.id); // "42"
|
||||
return div(`User ${params.id}`);
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Accessing Route Parameters Anywhere
|
||||
|
||||
The router maintains a reactive signal `router.params` that always holds the parameters of the currently matched route. You can read it anywhere in your app.
|
||||
|
||||
```javascript
|
||||
watch(() => {
|
||||
const params = router.params();
|
||||
console.log("Current route params:", params);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Navigation Utilities
|
||||
|
||||
SigPro provides a set of programmatic methods to control the history and read the state.
|
||||
SigPro provides several helper functions to control navigation and read the router state.
|
||||
|
||||
### `router.to(path)`
|
||||
|
||||
Navigates to the given path. It automatically formats the hash (e.g. `"/dashboard"` becomes `"#/dashboard"`). You can pass either a full hash string or a path without the `#`.
|
||||
|
||||
### `Router.to(path)`
|
||||
Navigates to a specific path. It automatically formats the hash (e.g., `/home` becomes `#/home`).
|
||||
```javascript
|
||||
Button({ onclick: () => Router.to("/dashboard") }, "Go to Dashboard")
|
||||
button({ onclick: () => router.to("/dashboard") }, "Go to Dashboard")
|
||||
```
|
||||
|
||||
### `Router.back()`
|
||||
Goes back to the previous page in the browser history.
|
||||
### `router.back()`
|
||||
|
||||
Goes back one step in the browser’s history, just like calling `history.back()`.
|
||||
|
||||
```javascript
|
||||
Button({ onclick: () => Router.back() }, "Back")
|
||||
button({ onclick: () => router.back() }, "← Back")
|
||||
```
|
||||
|
||||
### `Router.path()`
|
||||
Returns the current path (without the `#`).
|
||||
### `router.path()`
|
||||
|
||||
Returns the current route path **without the leading `#`**. This is a plain string, not a signal.
|
||||
|
||||
```javascript
|
||||
Watch(() => {
|
||||
console.log("Current path is:", Router.path());
|
||||
console.log(router.path()); // e.g. "/user/42"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Automatic Cleanup
|
||||
|
||||
Every time you navigate to a new route, the router calls `.destroy()` on the previous view. This recursively disposes of:
|
||||
|
||||
- All `watch` effects created inside that page
|
||||
- All event listeners attached via SigPro’s event binding
|
||||
- Any nested `when`, `each`, or `router` instances
|
||||
|
||||
**No manual cleanup is required** – memory leaks are prevented automatically.
|
||||
|
||||
---
|
||||
|
||||
## Reactive Route Parameters
|
||||
|
||||
`router.params` is a **reactive signal** (created with `$({})`). You can watch it to react to parameter changes without re‑mounting the whole router outlet.
|
||||
|
||||
```javascript
|
||||
watch(() => router.params(), (params) => {
|
||||
console.log("Params changed:", params);
|
||||
// e.g. fetch new data when the :id changes
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Technical Behavior
|
||||
## Styling the Router Outlet
|
||||
|
||||
* **Automatic Cleanup**: Every time you navigate, the router calls `.destroy()` on the previous view. This ensures that all **signals, effects, and event listeners** from the old page are purged from memory.
|
||||
* **Reactive Params**: `Router.params` is a signal (`$`). You can react to parameter changes without re-mounting the entire router outlet.
|
||||
* **Hash-Based**: By using `window.location.hash`, SigPro works out-of-the-box on any static hosting (like GitHub Pages or Vercel) without needing server-side redirects.
|
||||
|
||||
---
|
||||
|
||||
## Styling the transition of Router
|
||||
The router returns a standard `div` with the `.router-transition` class. You can easily style it or add transitions:
|
||||
The router returns a `div` with the class `"router-hook"`. You can style it just like any other element:
|
||||
|
||||
```css
|
||||
.router-transition {
|
||||
.router-hook {
|
||||
display: block;
|
||||
min-height: 100vh;
|
||||
animation: fadeIn 0.3s ease;
|
||||
min-height: 60vh;
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
```
|
||||
|
||||
If you want the router outlet to have no layout impact, you can set `display: contents` on it.
|
||||
|
||||
---
|
||||
|
||||
## Complete Example
|
||||
|
||||
```javascript
|
||||
import { $, router, mount } from "sigpro";
|
||||
|
||||
const Home = () => div("Welcome home");
|
||||
const About = () => div("About us");
|
||||
const User = (params) => div(`User profile: ${params.id}`);
|
||||
|
||||
const App = () =>
|
||||
div([
|
||||
nav([
|
||||
a({ href: "#/" }, "Home"),
|
||||
a({ href: "#/about" }, "About"),
|
||||
a({ href: "#/user/5" }, "User 5")
|
||||
]),
|
||||
router([
|
||||
{ path: "/", component: Home },
|
||||
{ path: "/about", component: About },
|
||||
{ path: "/user/:id", component: User },
|
||||
{ path: "*", component: () => div("404 – Not found") }
|
||||
])
|
||||
]);
|
||||
|
||||
mount(App, "#app");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Function | Description |
|
||||
| :--- | :--- |
|
||||
| `router(routes)` | Creates a router outlet. |
|
||||
| `router.to(path)` | Navigates to a new hash route. |
|
||||
| `router.back()` | Goes back in history. |
|
||||
| `router.path()` | Returns the current path without `#`. |
|
||||
| `router.params()` | Reactive signal of the current route parameters. |
|
||||
|
||||
@@ -25,21 +25,34 @@ $(computation: Function): ComputedSignal
|
||||
**`$(value)`**
|
||||
Creates a writable signal. It returns a function that acts as both **getter** and **setter**.
|
||||
|
||||
```javascript
|
||||
const count = $(0);
|
||||
<div id="demo-signal-simple"></div>
|
||||
|
||||
count(); // Read (0)
|
||||
count(10); // Write (10)
|
||||
```js
|
||||
{
|
||||
const count = $(0);
|
||||
const App = () => div({ class: "example" }, [
|
||||
p(() => `Count: ${count()}`),
|
||||
button({ onClick: () => count(count() + 1) }, "+1")
|
||||
]);
|
||||
setTimeout(() => mount(App, '#demo-signal-simple'), 50);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Persistent State
|
||||
**`$(value, key)`**
|
||||
Creates a writable signal that syncs with the browser's storage.
|
||||
|
||||
```javascript
|
||||
const theme = $("light", "app-theme");
|
||||
<div id="demo-signal-persist"></div>
|
||||
|
||||
theme("dark"); // Automatically calls localStorage.setItem("app-theme", '"dark"')
|
||||
```js
|
||||
{
|
||||
const theme = $("light", "theme-persist-demo");
|
||||
const App = () => div([
|
||||
p(() => `Current theme: ${theme()}`),
|
||||
button({ onClick: () => theme(theme() === "light" ? "dark" : "light") }, "Toggle theme")
|
||||
]);
|
||||
setTimeout(() => mount(App, '#demo-signal-persist'), 50);
|
||||
}
|
||||
```
|
||||
*Note: On page load, SigPro will prioritize the value found in `localStorage` over the `initialValue`.*
|
||||
|
||||
@@ -47,12 +60,23 @@ theme("dark"); // Automatically calls localStorage.setItem("app-theme", '"dark"'
|
||||
**`$(function)`**
|
||||
Creates a read-only signal that updates automatically when any signal used inside it changes.
|
||||
|
||||
```javascript
|
||||
<div id="demo-signal-computed"></div>
|
||||
|
||||
```js
|
||||
{
|
||||
const price = $(100);
|
||||
const tax = $(0.21);
|
||||
|
||||
// This tracks both 'price' and 'tax' automatically
|
||||
const total = $(() => price() * (1 + tax()));
|
||||
|
||||
const App = () => div([
|
||||
p(() => `Price: €${price()}`),
|
||||
p(() => `Tax rate: ${tax() * 100}%`),
|
||||
p(() => `Total: €${total().toFixed(2)}`),
|
||||
button({ onClick: () => price(price() + 10) }, "+€10"),
|
||||
button({ onClick: () => price(price() - 10) }, "-€10")
|
||||
]);
|
||||
setTimeout(() => mount(App, '#demo-signal-computed'), 50);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
@@ -60,11 +84,17 @@ const total = $(() => price() * (1 + tax()));
|
||||
## Updating with Logic
|
||||
When calling the setter, you can pass an **updater function** to access the current value safely.
|
||||
|
||||
```javascript
|
||||
const list = $(["A", "B"]);
|
||||
<div id="demo-signal-updater"></div>
|
||||
|
||||
// Adds "C" using the previous state
|
||||
list(prev => [...prev, "C"]);
|
||||
```js
|
||||
{
|
||||
const list = $(["A", "B"]);
|
||||
const App = () => div([
|
||||
ul(() => list().map(item => li(item))),
|
||||
button({ onClick: () => list(prev => [...prev, "C"]) }, "Add C")
|
||||
]);
|
||||
setTimeout(() => mount(App, '#demo-signal-updater'), 50);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
@@ -91,82 +121,92 @@ $$<T extends object>(obj: T): T
|
||||
|
||||
### 1. Simple Object
|
||||
|
||||
```javascript
|
||||
<div id="demo-dollar-simple"></div>
|
||||
|
||||
```js
|
||||
{
|
||||
const state = $$({ count: 0, name: "Juan" });
|
||||
watch(() => console.log(`Count is now ${state.count}`));
|
||||
|
||||
Watch(() => state.count, () => {
|
||||
console.log(`Count is now ${state.count}`);
|
||||
});
|
||||
|
||||
state.count++; // ✅ Triggers update
|
||||
state.name = "Ana"; // ✅ Also reactive
|
||||
const App = () => div([
|
||||
p(() => `Count: ${state.count}, Name: ${state.name}`),
|
||||
button({ onClick: () => state.count++ }, "Increment count"),
|
||||
button({ onClick: () => state.name = state.name === "Juan" ? "Ana" : "Juan" }, "Toggle name")
|
||||
]);
|
||||
setTimeout(() => mount(App, '#demo-dollar-simple'), 50);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Deep Reactivity
|
||||
|
||||
Unlike `$()`, `$$()` tracks nested properties automatically.
|
||||
<div id="demo-dollar-deep"></div>
|
||||
|
||||
```javascript
|
||||
```js
|
||||
{
|
||||
const user = $$({
|
||||
profile: {
|
||||
name: "Juan",
|
||||
address: {
|
||||
city: "Madrid",
|
||||
zip: "28001"
|
||||
}
|
||||
address: { city: "Madrid", zip: "28001" }
|
||||
}
|
||||
});
|
||||
|
||||
// This works! Tracks deep property access
|
||||
Watch(() => user.profile.address.city, () => {
|
||||
console.log("City changed");
|
||||
});
|
||||
watch(() => user.profile.address.city, () => console.log("City changed"));
|
||||
|
||||
user.profile.address.city = "Barcelona"; // ✅ Triggers update
|
||||
const App = () => div([
|
||||
p(() => `City: ${user.profile.address.city}`),
|
||||
button({ onClick: () => user.profile.address.city = "Barcelona" }, "Change to Barcelona")
|
||||
]);
|
||||
setTimeout(() => mount(App, '#demo-dollar-deep'), 50);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Arrays
|
||||
|
||||
`$$()` works with arrays and array methods.
|
||||
<div id="demo-dollar-array"></div>
|
||||
|
||||
```javascript
|
||||
```js
|
||||
{
|
||||
const todos = $$([
|
||||
{ id: 1, text: "Learn SigPro", done: false },
|
||||
{ id: 2, text: "Build an app", done: false }
|
||||
]);
|
||||
|
||||
Watch(() => todos.length, () => {
|
||||
console.log(`You have ${todos.length} todos`);
|
||||
});
|
||||
watch(() => todos.length, () => console.log(`You have ${todos.length} todos`));
|
||||
|
||||
// Array methods are reactive
|
||||
todos.push({ id: 3, text: "Deploy", done: false }); // ✅ Triggers
|
||||
todos[0].done = true; // ✅ Deep reactivity works
|
||||
todos.splice(1, 1); // ✅ Triggers
|
||||
const App = () => div([
|
||||
ul(() => todos.map(todo => li(todo.text + (todo.done ? " ✓" : "")))),
|
||||
button({ onClick: () => todos.push({ id: Date.now(), text: "New todo", done: false }) }, "Add todo"),
|
||||
button({ onClick: () => todos[0].done = !todos[0].done }, "Toggle first todo")
|
||||
]);
|
||||
setTimeout(() => mount(App, '#demo-dollar-array'), 50);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Mixed with Signals
|
||||
|
||||
`$$()` works seamlessly with `$()` signals.
|
||||
<div id="demo-dollar-mixed"></div>
|
||||
|
||||
```javascript
|
||||
```js
|
||||
{
|
||||
const form = $$({
|
||||
fields: {
|
||||
email: "",
|
||||
password: ""
|
||||
},
|
||||
isValid: $(false) // Signal inside reactive object
|
||||
fields: { email: "", password: "" },
|
||||
isValid: $(false)
|
||||
});
|
||||
|
||||
// Computed using both
|
||||
const canSubmit = $(() =>
|
||||
form.fields.email.includes("@") &&
|
||||
form.fields.password.length > 6
|
||||
);
|
||||
|
||||
Watch(canSubmit, (valid) => {
|
||||
form.isValid(valid); // Update signal inside reactive object
|
||||
});
|
||||
watch(canSubmit, valid => form.isValid(valid));
|
||||
|
||||
const App = () => div([
|
||||
input({ type: "email", placeholder: "Email", value: () => form.fields.email, onInput: e => form.fields.email = e.target.value }),
|
||||
input({ type: "password", placeholder: "Password", value: () => form.fields.password, onInput: e => form.fields.password = e.target.value }),
|
||||
p(() => `Form valid: ${form.isValid() ? "Yes" : "No"}`)
|
||||
]);
|
||||
setTimeout(() => mount(App, '#demo-dollar-mixed'), 50);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
@@ -189,27 +229,45 @@ Watch(canSubmit, (valid) => {
|
||||
## When to Use Each
|
||||
|
||||
### Use `$()` when:
|
||||
- Working with primitives (numbers, strings, booleans)
|
||||
- Need localStorage persistence
|
||||
- Creating computed values
|
||||
- Want explicit control over updates
|
||||
|
||||
```javascript
|
||||
<div id="demo-use-dollar"></div>
|
||||
|
||||
```js
|
||||
{
|
||||
const count = $(0);
|
||||
const user = $(null);
|
||||
const firstName = $("John");
|
||||
const lastName = $("Doe");
|
||||
const fullName = $(() => `${firstName()} ${lastName()}`);
|
||||
|
||||
const App = () => div([
|
||||
p(() => `Count: ${count()}`),
|
||||
button({ onClick: () => count(count() + 1) }, "Count up"),
|
||||
p(() => `Full name: ${fullName()}`),
|
||||
input({ value: firstName, placeholder: "First name" }),
|
||||
input({ value: lastName, placeholder: "Last name" })
|
||||
]);
|
||||
setTimeout(() => mount(App, '#demo-use-dollar'), 50);
|
||||
}
|
||||
```
|
||||
|
||||
### Use `$$()` when:
|
||||
- Working with complex nested objects
|
||||
- Managing forms with multiple fields
|
||||
- Using arrays with mutations (push, pop, splice)
|
||||
- Want natural object syntax (no function calls)
|
||||
|
||||
```javascript
|
||||
<div id="demo-use-dollar-dollar"></div>
|
||||
|
||||
```js
|
||||
{
|
||||
const form = $$({ email: "", password: "" });
|
||||
const settings = $$({ theme: "dark", notifications: true });
|
||||
const store = $$({ users: [], filters: {}, pagination: { page: 1 } });
|
||||
|
||||
const App = () => div([
|
||||
input({ placeholder: "Email", onInput: e => form.email = e.target.value }),
|
||||
input({ placeholder: "Password", type: "password", onInput: e => form.password = e.target.value }),
|
||||
p(() => `Email: ${form.email}, Password: ${form.password}`),
|
||||
button({ onClick: () => settings.theme = settings.theme === "dark" ? "light" : "dark" }, "Toggle theme"),
|
||||
p(() => `Current theme: ${settings.theme}`)
|
||||
]);
|
||||
setTimeout(() => mount(App, '#demo-use-dollar-dollar'), 50);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
@@ -217,19 +275,19 @@ const store = $$({ users: [], filters: {}, pagination: { page: 1 } });
|
||||
## Important Notes
|
||||
|
||||
### ✅ DO:
|
||||
```javascript
|
||||
```js
|
||||
// Access properties directly
|
||||
state.count = 10;
|
||||
state.user.name = "Ana";
|
||||
todos.push(newItem);
|
||||
|
||||
// Track in effects
|
||||
Watch(() => state.count, () => {});
|
||||
Watch(() => state.user.name, () => {});
|
||||
watch(() => state.count, () => {});
|
||||
watch(() => state.user.name, () => {});
|
||||
```
|
||||
|
||||
### ❌ DON'T:
|
||||
```javascript
|
||||
```js
|
||||
// Destructuring breaks reactivity
|
||||
const { count, user } = state; // ❌ count and user are not reactive
|
||||
|
||||
@@ -248,7 +306,7 @@ Like all SigPro reactive primitives, `$$()` integrates with the cleanup system:
|
||||
|
||||
- Effects tracking reactive properties are automatically disposed
|
||||
- No manual cleanup needed
|
||||
- Works with `Watch`, `If`, and `For`
|
||||
- Works with `watch`, `when`, and `each`
|
||||
|
||||
---
|
||||
|
||||
@@ -266,32 +324,19 @@ Like all SigPro reactive primitives, `$$()` integrates with the cleanup system:
|
||||
|
||||
## Complete Example
|
||||
|
||||
```javascript
|
||||
// Combining both approaches
|
||||
<div id="demo-complete"></div>
|
||||
|
||||
```js
|
||||
{
|
||||
const app = {
|
||||
// Simple primitives with persistence
|
||||
theme: $("dark", "theme"),
|
||||
theme: $("dark", "theme_complete"),
|
||||
sidebarOpen: $(true),
|
||||
|
||||
// Complex state with $$()
|
||||
user: $$({
|
||||
name: "",
|
||||
email: "",
|
||||
preferences: {
|
||||
notifications: true,
|
||||
language: "es"
|
||||
}
|
||||
}),
|
||||
|
||||
// Computed values
|
||||
user: $$({ name: "", email: "", preferences: { notifications: true, language: "es" } }),
|
||||
isLoggedIn: $(() => !!app.user.name),
|
||||
|
||||
// Actions
|
||||
login(name, email) {
|
||||
app.user.name = name;
|
||||
app.user.email = email;
|
||||
},
|
||||
|
||||
logout() {
|
||||
app.user.name = "";
|
||||
app.user.email = "";
|
||||
@@ -299,23 +344,26 @@ const app = {
|
||||
}
|
||||
};
|
||||
|
||||
// UI component
|
||||
const UserProfile = () => {
|
||||
return Div({}, [
|
||||
If(() => app.isLoggedIn(),
|
||||
() => Div({}, [
|
||||
H2(`Welcome ${app.user.name}`),
|
||||
P(`Email: ${app.user.email}`),
|
||||
P(`Notifications: ${app.user.preferences.notifications ? "ON" : "OFF"}`),
|
||||
Button({ onclick: () => app.user.preferences.notifications = !app.user.preferences.notifications },
|
||||
"Toggle Notifications"
|
||||
),
|
||||
Button({ onclick: app.logout }, "Logout")
|
||||
]),
|
||||
() => LoginForm()
|
||||
)
|
||||
const LoginForm = () => div([
|
||||
input({ placeholder: "Name", onInput: e => app.user.name = e.target.value }),
|
||||
input({ placeholder: "Email", onInput: e => app.user.email = e.target.value }),
|
||||
button({ onClick: () => app.login(app.user.name, app.user.email) }, "Login")
|
||||
]);
|
||||
};
|
||||
|
||||
const UserProfile = () => div([
|
||||
h2(() => `Welcome ${app.user.name}`),
|
||||
p(() => `Email: ${app.user.email}`),
|
||||
p(() => `Notifications: ${app.user.preferences.notifications ? "ON" : "OFF"}`),
|
||||
button({ onClick: () => app.user.preferences.notifications = !app.user.preferences.notifications }, "Toggle Notifications"),
|
||||
button({ onClick: app.logout }, "Logout")
|
||||
]);
|
||||
|
||||
const App = () => div({ class: "complete-example" }, [
|
||||
when(() => app.isLoggedIn(), () => UserProfile(), () => LoginForm())
|
||||
]);
|
||||
|
||||
setTimeout(() => mount(App, '#demo-complete'), 50);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
@@ -324,7 +372,7 @@ const UserProfile = () => {
|
||||
|
||||
If you have code using nested signals:
|
||||
|
||||
```javascript
|
||||
```js
|
||||
// Before - Manual nesting
|
||||
const user = $({
|
||||
name: $(""),
|
||||
@@ -339,3 +387,4 @@ const user = $$({
|
||||
});
|
||||
user.name = "Juan"; // Direct assignment
|
||||
```
|
||||
|
||||
|
||||
206
docs/api/tags.md
206
docs/api/tags.md
@@ -1,49 +1,56 @@
|
||||
# Global Tag Helpers
|
||||
|
||||
In **SigPro**, you don't need to manually type `Tag('div', ...)` for every element. To keep your code declarative and readable, the engine automatically generates **Global Helper Functions** for all standard HTML5 tags upon initialization.
|
||||
In **SigPro**, you don't need to manually type `h('div', ...)` for every element. To keep your code declarative and readable, the engine automatically generates **helper functions** for all standard HTML5 tags upon initialization.
|
||||
|
||||
## 1. How it Works
|
||||
|
||||
SigPro iterates through a manifest of standard HTML tags and attaches a wrapper function for each one directly to the `window` object. This creates a specialized **DSL** (Domain Specific Language) that looks like a template engine but is **100% standard JavaScript**.
|
||||
SigPro iterates through a list of standard HTML tags and attaches a wrapper function for each one directly to the `window` object. This creates a specialized **DSL** (Domain Specific Language) that looks like a template engine but is **100% standard JavaScript**.
|
||||
|
||||
* **Under the hood:** `Tag('button', { onclick: ... }, 'Click')`
|
||||
* **SigPro Style:** `Button({ onclick: ... }, 'Click')`
|
||||
* **Under the hood:** `h('button', { onclick: ... }, 'Click')`
|
||||
* **SigPro Style:** `button({ onclick: ... }, 'Click')`
|
||||
|
||||
> **Note:** All tag helpers are **lowercase** (e.g. `div`, `span`, `button`). PascalCase versions (`Div`, `Span`, `Button`) are **not** created. This keeps the syntax close to raw HTML.
|
||||
|
||||
---
|
||||
|
||||
## 2. The Complete Global Registry
|
||||
|
||||
The following functions are injected into the global scope using **PascalCase** to prevent naming collisions with common JS variables:
|
||||
The following functions are injected into the global scope using **lowercase** names to match HTML tags:
|
||||
|
||||
| Category | Available Global Functions |
|
||||
| :--- | :--- |
|
||||
| **Structure** | `Div`, `Span`, `P`, `Section`, `Nav`, `Main`, `Header`, `Footer`, `Article`, `Aside` |
|
||||
| **Typography** | `H1` to `H6`, `Ul`, `Ol`, `Li`, `Dl`, `Dt`, `Dd`, `Strong`, `Em`, `Code`, `Pre`, `Small`, `B`, `U`, `Mark` |
|
||||
| **Interactive** | `Button`, `A`, `Label`, `Br`, `Hr`, `Details`, `Summary`, `Dialog` |
|
||||
| **Forms** | `Form`, `Input`, `Select`, `Option`, `Textarea`, `Fieldset`, `Legend` |
|
||||
| **Tables** | `Table`, `Thead`, `Tbody`, `Tr`, `Th`, `Td`, `Tfoot`, `Caption` |
|
||||
| **Media** | `Img`, `Canvas`, `Video`, `Audio`, `Svg`, `Iframe`, `Picture`, `Source` |
|
||||
| **Structure** | `div`, `span`, `p`, `section`, `nav`, `main`, `header`, `footer`, `article`, `aside` |
|
||||
| **Typography** | `h1`, `h2`, `h3`, `h4`, `h5`, `h6`, `ul`, `ol`, `li`, `dl`, `dt`, `dd`, `strong`, `em`, `code`, `pre`, `small`, `b`, `u`, `mark` |
|
||||
| **Interactive** | `button`, `a`, `label`, `br`, `hr`, `details`, `summary`, `dialog` |
|
||||
| **Forms** | `form`, `input`, `select`, `option`, `textarea`, `fieldset`, `legend` |
|
||||
| **Tables** | `table`, `thead`, `tbody`, `tr`, `th`, `td`, `tfoot`, `caption` |
|
||||
| **Media** | `img`, `canvas`, `video`, `audio`, `svg`, `iframe`, `picture`, `source` |
|
||||
|
||||
Full list includes: `a`, `abbr`, `article`, `aside`, `audio`, `b`, `blockquote`, `br`, `button`, `canvas`, `caption`, `cite`, `code`, `col`, `colgroup`, `datalist`, `dd`, `del`, `details`, `dfn`, `dialog`, `div`, `dl`, `dt`, `em`, `embed`, `fieldset`, `figcaption`, `figure`, `footer`, `form`, `h1`…`h6`, `header`, `hr`, `i`, `iframe`, `img`, `input`, `ins`, `kbd`, `label`, `legend`, `li`, `main`, `mark`, `meter`, `nav`, `object`, `ol`, `optgroup`, `option`, `output`, `p`, `picture`, `pre`, `progress`, `section`, `select`, `slot`, `small`, `source`, `span`, `strong`, `sub`, `summary`, `sup`, `svg`, `table`, `tbody`, `td`, `template`, `textarea`, `tfoot`, `th`, `thead`, `time`, `tr`, `u`, `ul`, `video`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Usage Patterns (Smart Arguments)
|
||||
## 3. Usage Patterns
|
||||
|
||||
SigPro tag helpers are flexible. They automatically detect if you are passing attributes, children, or both.
|
||||
|
||||
### A. Attributes + Children
|
||||
|
||||
```javascript
|
||||
Div({ class: 'container', id: 'main' }, [
|
||||
H1("Welcome to SigPro"),
|
||||
P("The zero-VDOM framework.")
|
||||
div({ class: 'container', id: 'main' }, [
|
||||
h1("Welcome to SigPro"),
|
||||
p("The zero-VDOM framework.")
|
||||
]);
|
||||
```
|
||||
|
||||
### B. Children Only (The "Skipper")
|
||||
### B. Children Only
|
||||
|
||||
If you don't need attributes, you can pass the content directly as the first argument.
|
||||
|
||||
```javascript
|
||||
Section([
|
||||
H2("Clean Syntax"),
|
||||
Button("I have no props!")
|
||||
section([
|
||||
h2("Clean Syntax"),
|
||||
button("I have no props!")
|
||||
]);
|
||||
```
|
||||
|
||||
@@ -51,139 +58,146 @@ Section([
|
||||
|
||||
## 4. Reactive Power
|
||||
|
||||
These helpers are natively wired into SigPro's **`Watch`** engine.
|
||||
These helpers are natively wired into SigPro's reactivity system.
|
||||
|
||||
### Reactive Attributes (One‑Way)
|
||||
|
||||
Pass a **function** that returns the value. SigPro creates an internal effect to keep the DOM in sync.
|
||||
|
||||
### Reactive Attributes (One-Way)
|
||||
Simply pass a Signal (function) to any attribute. SigPro creates an internal `Watch` to keep the DOM in sync.
|
||||
```javascript
|
||||
const theme = $("light");
|
||||
|
||||
Div({
|
||||
div({
|
||||
class: () => `app-box ${theme()}`
|
||||
}, "Themeable Box");
|
||||
```
|
||||
|
||||
### Smart Two-Way Binding (Automatic)
|
||||
SigPro automatically bridges the **Signal** and the **Input** element bi-directionally when you assign a Signal to `value` or `checked`. No special operators are required.
|
||||
### Two‑Way Binding (Automatic)
|
||||
|
||||
SigPro automatically bridges the **signal** and the **input element** bidirectionally when you assign a signal to `value` or `checked`.
|
||||
|
||||
```javascript
|
||||
const search = $("");
|
||||
|
||||
// UI updates Signal AND Signal updates UI automatically
|
||||
Input({
|
||||
input({
|
||||
type: "text",
|
||||
placeholder: "Search...",
|
||||
value: search
|
||||
});
|
||||
```
|
||||
|
||||
> **Pro Tip:** If you want an input to be **read-only** but still reactive, wrap the signal in an anonymous function: `value: () => search()`. This prevents the "backwards" synchronization.
|
||||
> **Pro Tip:** If you want an input to be **read‑only** but still reactive, wrap the signal in an anonymous function: `value: () => search()`. This prevents backward synchronization.
|
||||
|
||||
### Dynamic Children
|
||||
|
||||
You can pass a function as a child – it will be re‑executed whenever any signal inside changes, and the DOM will be patched surgically.
|
||||
|
||||
### Dynamic Flow & Cleanup
|
||||
Combine tags with Core controllers. SigPro automatically cleans up the `Watch` instances and event listeners when nodes are removed from the DOM.
|
||||
```javascript
|
||||
const items = $(["Apple", "Banana", "Cherry"]);
|
||||
const count = $(0);
|
||||
|
||||
Ul({ class: "list-disc" }, [
|
||||
For(items, (item) => Li(item), (item) => item)
|
||||
div([
|
||||
p(() => `Count is ${count()}`),
|
||||
button({ onClick: () => count(count() + 1) }, "Increment")
|
||||
]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<div class="alert alert-error shadow-lg my-8 border-l-8 border-error bg-error/10 text-error-content">
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
|
||||
<div>
|
||||
<h3 class="font-bold text-lg">Important: Naming Conventions</h3>
|
||||
<div class="text-sm opacity-90">
|
||||
<ol class="list-decimal ml-4 mt-2 space-y-2">
|
||||
<li><b>Avoid Shadowing:</b> Don't name your local variables like the tags (e.g., <code>const Div = ...</code>). This will "hide" the global SigPro helper.</li>
|
||||
<li><b>Custom Components:</b> Always use <b>PascalCase</b> for your own component functions (e.g., <code>UserCard</code>, <code>NavMenu</code>) to distinguish them from built-in Tag Helpers.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
## 5. Custom Components with `h()`
|
||||
|
||||
---
|
||||
While the global tag helpers cover all standard HTML tags, you can create reusable components using the `h` function directly (or by returning the result of tag helpers).
|
||||
|
||||
## 5. Logic to UI Comparison
|
||||
|
||||
Here is how a dynamic **User Status** component translates from SigPro logic to the final DOM structure.
|
||||
|
||||
```javascript
|
||||
const UserStatus = (name, online) => (
|
||||
Div({ class: 'flex items-center gap-2' }, [
|
||||
Span({
|
||||
hidden: () => !online(),
|
||||
class: 'w-3 h-3 bg-green-500 rounded-full'
|
||||
}),
|
||||
P({
|
||||
class: () => online() ? "text-bold" : "text-gray-400"
|
||||
}, name)
|
||||
])
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Custom Tags with `Tag`
|
||||
|
||||
Create reusable components using `Tag`. All reactivity auto-cleans itself.
|
||||
|
||||
### Basic Example
|
||||
### Basic Component
|
||||
|
||||
```javascript
|
||||
const UserCard = (props, children) =>
|
||||
Tag('div', { class: 'card p-4', 'data-id': props.id }, children);
|
||||
div({ class: 'card p-4', 'data-id': props.id }, children);
|
||||
|
||||
UserCard({ id: 123 }, [H3("John Doe"), P("john@example.com")]);
|
||||
UserCard({ id: 123 }, [h3("John Doe"), p("john@example.com")]);
|
||||
```
|
||||
|
||||
### Reactive Component (Auto-Cleaned)
|
||||
### Reactive Component
|
||||
|
||||
```javascript
|
||||
const Counter = (initial = 0) => {
|
||||
const count = $(initial);
|
||||
return Tag('div', { class: 'flex gap-2' }, [
|
||||
Button({ onclick: () => count(count() - 1) }, '-'),
|
||||
Span(() => count()),
|
||||
Button({ onclick: () => count(count() + 1) }, '+')
|
||||
const Counter = () => {
|
||||
const count = $(0);
|
||||
return div({ class: 'flex gap-2' }, [
|
||||
button({ onClick: () => count(count() - 1) }, '-'),
|
||||
span(() => count()),
|
||||
button({ onClick: () => count(count() + 1) }, '+')
|
||||
]);
|
||||
};
|
||||
```
|
||||
|
||||
### When Manual Cleanup is Needed
|
||||
### Manual Cleanup for External Resources
|
||||
|
||||
Only for external resources (intervals, sockets, third-party libs):
|
||||
Only needed for intervals, sockets, third‑party libraries:
|
||||
|
||||
```javascript
|
||||
const Timer = () => {
|
||||
const time = $(new Date().toLocaleTimeString());
|
||||
const el = Tag('span', {}, () => time());
|
||||
const el = span(() => time());
|
||||
|
||||
const interval = setInterval(() => time(new Date().toLocaleTimeString()), 1000);
|
||||
el._cleanups.add(() => clearInterval(interval)); // Manual cleanup
|
||||
onUnmount(() => clearInterval(interval));
|
||||
|
||||
return el;
|
||||
};
|
||||
```
|
||||
|
||||
### `Tag` vs Tag Helpers
|
||||
---
|
||||
|
||||
| Use | Recommendation |
|
||||
## 6. Comparison with `h()`
|
||||
|
||||
| Use case | Recommendation |
|
||||
| :--- | :--- |
|
||||
| Standard tags (`div`, `span`) | `Div()`, `Span()` helpers |
|
||||
| Reusable components | Function returning `Tag` |
|
||||
| Dynamic tag names | `Tag(tagName, props, children)` |
|
||||
| Standard tags (`div`, `span`, `button`) | Use global helpers: `div()`, `span()`, `button()` |
|
||||
| Dynamic tag names (unknown at write time) | Use `h(tagName, props, children)` |
|
||||
| Components returning a single node | Any function that returns a node (using helpers or `h`) |
|
||||
|
||||
> **Auto-cleanup**: `Tag` automatically destroys watchers, events, and nested components. Only add to `_cleanups` for external resources.
|
||||
> **Auto‑cleanup:** All tag helpers and `h` automatically dispose effects, event listeners, and nested components when removed from the DOM.
|
||||
|
||||
---
|
||||
|
||||
| State (`online`) | Rendered HTML | Memory Management |
|
||||
| :--- | :--- | :--- |
|
||||
| **`true`** | `<div class="flex..."><span class="w-3..."></span><p class="text-bold">John</p></div>` | Watcher active |
|
||||
| **`false`** | `<div class="flex..."><span hidden class="w-3..."></span><p class="text-gray-400">John</p></div>` | Attribute synced |
|
||||
## 7. Complete Example
|
||||
|
||||
```javascript
|
||||
const App = () =>
|
||||
div({ class: "app" }, [
|
||||
h1("Welcome"),
|
||||
input({
|
||||
placeholder: "Your name",
|
||||
value: nameSignal,
|
||||
onInput: (e) => nameSignal(e.target.value)
|
||||
}),
|
||||
p(() => `Hello, ${nameSignal() || "stranger"}!`),
|
||||
button({ onClick: () => alert("Clicked") }, "Click me")
|
||||
]);
|
||||
|
||||
mount(App, '#app');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<div class="alert alert-info">
|
||||
<div>
|
||||
<h3>Important Notes</h3>
|
||||
<ul>
|
||||
<li><b>Naming:</b> All tag helpers are <b>lowercase</b>. There are no PascalCase helpers (<code>Div</code>, <code>Button</code>).</li>
|
||||
<li><b>Global availability:</b> After importing SigPro (via <code>import 'sigpro'</code> or CDN), all helpers are on <code>window</code>. You can use them anywhere without importing.</li>
|
||||
<li><b>Custom components:</b> Use PascalCase for your own component functions to visually distinguish them from built‑in tags (e.g., <code>UserCard</code>).</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 8. Summary
|
||||
|
||||
| Feature | Description |
|
||||
| :--- | :--- |
|
||||
| **Tag helpers** | Lowercase functions for every HTML element (e.g., `div()`, `button()`). |
|
||||
| **Reactive attributes** | Pass a function to any attribute to keep it synced. |
|
||||
| **Two‑way binding** | Assign a signal directly to `value` or `checked` on form elements. |
|
||||
| **Dynamic children** | Pass a function as a child for live updating content. |
|
||||
| **Auto‑cleanup** | All effects, events, and children are disposed when the element is removed. |
|
||||
@@ -1,21 +1,22 @@
|
||||
# Reactivity Control: `Watch( )`
|
||||
# Reactivity Control: `watch( )`
|
||||
|
||||
The `Watch` function is the reactive engine of SigPro. It allows you to execute code automatically when signals change. `Watch` is **polymorphic**: it can track dependencies automatically or follow an explicit list.
|
||||
The `watch` function is the reactive engine of SigPro. It allows you to execute code automatically when signals change. `watch` is **polymorphic**: it can track dependencies automatically or follow an explicit list.
|
||||
|
||||
## Function Signature
|
||||
|
||||
```typescript
|
||||
// Automatic Mode (Magic Tracking)
|
||||
Watch(callback: Function): StopFunction
|
||||
watch(callback: Function): StopFunction
|
||||
|
||||
// Explicit Mode (Isolated Dependencies)
|
||||
Watch(deps: Signal[], callback: Function): StopFunction
|
||||
watch(deps: Signal[], callback: (values: any[]) => void): StopFunction
|
||||
```
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **`target / deps`** | `Function` | `Array` | Yes | Either the code to run (Auto) or an array of signals to watch (Explicit). |
|
||||
| **`callback`** | `Function` | Only in Explicit | The code that will run when the `deps` change. |
|
||||
| **`callback`** (auto mode) | `Function` | Yes | The code to run. Any signal accessed inside becomes a dependency. |
|
||||
| **`deps`** (explicit mode) | `Signal[]` | Yes | An array of signals to watch explicitly. |
|
||||
| **`callback`** (explicit mode) | `Function` | Yes | Runs when any of the `deps` change. Receives an array of their current values. |
|
||||
|
||||
**Returns:** A `StopFunction` that, when called, destroys the watcher and releases memory.
|
||||
|
||||
@@ -24,68 +25,99 @@ Watch(deps: Signal[], callback: Function): StopFunction
|
||||
## Usage Patterns
|
||||
|
||||
### 1. Automatic Mode (Default)
|
||||
Any signal you "touch" inside the callback becomes a dependency. SigPro tracks them behind the scenes.
|
||||
Any signal you **touch** inside the callback becomes a dependency. SigPro tracks them behind the scenes.
|
||||
|
||||
```javascript
|
||||
const count = $(0);
|
||||
|
||||
Watch(() => {
|
||||
// Re-runs every time 'count' changes
|
||||
watch(() => {
|
||||
// Re‑runs every time 'count' changes
|
||||
console.log(`Count is: ${count()}`);
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Explicit Mode (Advanced Cleanup)
|
||||
This mode **isolates** execution. The callback only triggers when the signals in the array change. Any other signal accessed *inside* the callback will NOT trigger a re-run. This is the "gold standard" for Routers and heavy components.
|
||||
### 2. Explicit Mode (Isolated)
|
||||
This mode **isolates** execution. The callback only triggers when the signals in the array change. Any other signal accessed *inside* the callback will **not** trigger a re‑run. This is ideal for routers or performance‑critical components.
|
||||
|
||||
```javascript
|
||||
const sPath = $("/home");
|
||||
const path = $("/home");
|
||||
const user = $("Admin");
|
||||
|
||||
Watch([sPath], () => {
|
||||
// Only triggers when 'sPath' changes.
|
||||
// Changes to 'user' will NOT trigger this, preventing accidental re-renders.
|
||||
console.log(`Navigating to ${sPath()} as ${user()}`);
|
||||
watch([path], ([newPath]) => {
|
||||
// Only triggers when 'path' changes.
|
||||
// Changes to 'user' will NOT trigger this.
|
||||
console.log(`Navigating to ${newPath} as ${user()}`);
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Automatic Cleanup
|
||||
If your logic creates timers, event listeners, or other reactive effects, SigPro tracks them as "children" of the current watch. When the watcher re-runs or stops, it kills everything inside automatically.
|
||||
In explicit mode, the callback receives an array of current values corresponding to the `deps` order.
|
||||
|
||||
### 3. Stopping a Watcher
|
||||
Call the returned function to kill the watcher manually.
|
||||
|
||||
```javascript
|
||||
Watch(() => {
|
||||
const timer = setInterval(() => console.log("Tick"), 1000);
|
||||
|
||||
// Register a manual cleanup if needed
|
||||
// Or simply rely on SigPro to kill nested Watch() calls
|
||||
return () => clearInterval(timer);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stopping a Watcher
|
||||
Call the returned function to manually kill the watcher. This is essential for manual DOM injections (like Toasts) or long-lived background processes.
|
||||
|
||||
```javascript
|
||||
const stop = Watch(() => console.log(count()));
|
||||
|
||||
const stop = watch(() => console.log(count()));
|
||||
// Later...
|
||||
stop(); // The link between the signal and this code is physically severed.
|
||||
stop(); // Disconnects the watcher completely.
|
||||
```
|
||||
|
||||
### 4. Automatic Cleanup Inside Effects
|
||||
If your watcher creates timers, event listeners, or nested effects, SigPro tracks them as children and cleans them up automatically before re‑running or when stopped.
|
||||
|
||||
```javascript
|
||||
watch(() => {
|
||||
const timer = setInterval(() => console.log("tick"), 1000);
|
||||
// No need to manually clear – SigPro will dispose it when the watcher re‑runs or stops.
|
||||
// (But you can also return a cleanup function if needed)
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pro Tip: The Microtask Queue
|
||||
SigPro batches updates. If you update multiple signals in the same execution block, the watcher will only fire **once** at the end of the task.
|
||||
## Batching & Microtask Queue
|
||||
|
||||
SigPro batches reactive updates. If you modify several signals in the same synchronous block, the watcher will fire **only once**, after the task completes.
|
||||
|
||||
```javascript
|
||||
const a = $(0);
|
||||
const b = $(0);
|
||||
|
||||
Watch(() => console.log(a(), b()));
|
||||
watch(() => console.log(a(), b()));
|
||||
|
||||
// This triggers only ONE re-run.
|
||||
// Triggers only ONE log: "1 2"
|
||||
a(1);
|
||||
b(2);
|
||||
```
|
||||
|
||||
This is achieved via `queueMicrotask`, ensuring optimal performance.
|
||||
|
||||
---
|
||||
|
||||
## Key Points
|
||||
|
||||
- **Function name:** `watch` (lowercase) – exported from SigPro and also available globally.
|
||||
- **Auto mode:** `watch(fn)` – automatically tracks any signals read inside `fn`.
|
||||
- **Explicit mode:** `watch([sig1, sig2], (values) => {...})` – only re‑runs when listed signals change; callback receives an array of their new values.
|
||||
- **Stop function:** returned by both modes; call it to dispose the effect and its children.
|
||||
- **Batching:** multiple signal writes in one event loop tick trigger a single execution (microtask).
|
||||
|
||||
---
|
||||
|
||||
## Complete Example
|
||||
|
||||
```javascript
|
||||
const count = $(0);
|
||||
const step = $(1);
|
||||
|
||||
watch(() => {
|
||||
console.log(`Count changed to ${count()}`);
|
||||
});
|
||||
|
||||
watch([count, step], ([newCount, newStep]) => {
|
||||
console.log(`Count=${newCount}, step=${newStep} (explicit)`);
|
||||
});
|
||||
|
||||
count(5); // logs: auto + explicit
|
||||
step(2); // logs: explicit only (auto does not track step)
|
||||
```
|
||||
|
||||
|
||||
132
docs/api/when.md
Normal file
132
docs/api/when.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Reactive Branching: `when( )`
|
||||
|
||||
The `when` function is the reactive control flow operator in SigPro. It conditionally renders one of two branches (or nothing) based on a reactive condition. The inactive branch is completely removed from the DOM and its effects are destroyed, saving memory and CPU.
|
||||
|
||||
## Function Signature
|
||||
|
||||
```typescript
|
||||
when(
|
||||
condition: boolean | Signal<boolean> | (() => boolean),
|
||||
thenBranch: Node | (() => Node),
|
||||
elseBranch?: Node | (() => Node) | null
|
||||
): HTMLElement
|
||||
```
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **`condition`** | `boolean` / `Signal` / `() => boolean` | Yes | A value or reactive expression that determines which branch to show. |
|
||||
| **`thenBranch`** | `Node` / `() => Node` | Yes | Content rendered when the condition is **truthy**. |
|
||||
| **`elseBranch`** | `Node` / `() => Node` | No | Content rendered when the condition is **falsy**. Defaults to nothing. |
|
||||
|
||||
**Returns:** A `div` with `style="display:contents"` that acts as an anchor for the dynamic content. This element is part of the DOM and will be replaced/updated automatically.
|
||||
|
||||
> **Note:** The function name is `when` (lowercase). It is exported from SigPro and also available globally after importing.
|
||||
|
||||
---
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### 1. Simple Toggle
|
||||
|
||||
```javascript
|
||||
const isVisible = $(false);
|
||||
|
||||
div([
|
||||
button({ onclick: () => isVisible(!isVisible()) }, "Toggle"),
|
||||
when(isVisible,
|
||||
p("Now you see me!"),
|
||||
p("Now you don't...")
|
||||
)
|
||||
]);
|
||||
```
|
||||
|
||||
### 2. With Functions (Lazy Initialization)
|
||||
|
||||
To avoid creating heavy components until they are actually needed, pass a function that returns the branch.
|
||||
|
||||
```javascript
|
||||
when(() => user.isLoggedIn(),
|
||||
() => DashboardComponent(), // Only created when logged in
|
||||
() => LoginForm() // Only created when guest
|
||||
);
|
||||
```
|
||||
|
||||
### 3. Complex Conditions
|
||||
|
||||
`condition` can be any expression that returns a boolean – it can read signals, computed values, or plain booleans.
|
||||
|
||||
```javascript
|
||||
when(() => count() > 10 && status() === 'ready',
|
||||
span("Threshold reached!")
|
||||
);
|
||||
```
|
||||
|
||||
### 4. Without an `else` branch
|
||||
|
||||
If no `elseBranch` is provided, nothing is rendered when the condition is falsy.
|
||||
|
||||
```javascript
|
||||
when(loading,
|
||||
div({ class: "spinner" }, "Loading...")
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Automatic Cleanup
|
||||
|
||||
`when` automatically manages the lifecycle of each branch:
|
||||
|
||||
- When the condition changes, the current branch is destroyed.
|
||||
- All effects (`watch`), event listeners, and child `when`/`each` inside the destroyed branch are recursively disposed.
|
||||
- The new branch is created and mounted.
|
||||
- Memory leaks are prevented without any manual intervention.
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Use functions for expensive branches** – `() => Component()` ensures the component is only created when the branch becomes active.
|
||||
- **Avoid inline complex logic** – keep conditions readable; extract to computed signals if needed.
|
||||
- **No manual cleanup required** – SigPro handles everything.
|
||||
|
||||
---
|
||||
|
||||
## Technical Comparison
|
||||
|
||||
| Feature | CSS `display: none` | `when` |
|
||||
| :--- | :--- | :--- |
|
||||
| **DOM presence** | Always present | Only active branch exists |
|
||||
| **Event listeners** | Still attached | Removed |
|
||||
| **Effects (`watch`)** | Still running | Destroyed |
|
||||
| **Memory usage** | Higher | Optimised (only one branch alive) |
|
||||
| **Cleanup** | Manual | Automatic |
|
||||
|
||||
---
|
||||
|
||||
## Complete Example
|
||||
|
||||
```javascript
|
||||
const loggedIn = $(false);
|
||||
const username = $("Guest");
|
||||
|
||||
const Profile = () => div([
|
||||
h2(`Welcome, ${username()}`),
|
||||
button({ onclick: () => loggedIn(false) }, "Logout")
|
||||
]);
|
||||
|
||||
const LoginForm = () => div([
|
||||
input({ placeholder: "Name", onInput: e => username(e.target.value) }),
|
||||
button({ onclick: () => loggedIn(true) }, "Login")
|
||||
]);
|
||||
|
||||
const App = () =>
|
||||
div([
|
||||
when(loggedIn,
|
||||
() => Profile(),
|
||||
() => LoginForm()
|
||||
)
|
||||
]);
|
||||
|
||||
mount(App, '#app');
|
||||
```
|
||||
137
docs/examples.md
137
docs/examples.md
@@ -1,137 +0,0 @@
|
||||
# Interactive Examples
|
||||
|
||||
Explore the power of SigPro through practical patterns. From basic reactivity to advanced composition.
|
||||
NOTE: Here we use DaisyUI for styles.
|
||||
|
||||
---
|
||||
|
||||
## 1. Basic Reactivity
|
||||
The classic counter. Notice how we use `$(0)` to create a signal and the `Button` tag helper to update it.
|
||||
|
||||
<div class="card bg-base-200 border border-base-300 shadow-sm my-6">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-sm uppercase opacity-50 mb-4">Live Demo</h3>
|
||||
<div id="demo-counter" class="bg-base-100 p-6 rounded-xl border border-base-300 flex items-center justify-center"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
```javascript
|
||||
const Counter = () => {
|
||||
const $count = $(0);
|
||||
return Div({ class: 'flex gap-4 items-center' }, [
|
||||
Button({ class: 'btn btn-circle btn-outline', onclick: () => $count(c => c - 1) }, "-"),
|
||||
Span({ class: 'text-2xl font-mono w-12 text-center' }, $count),
|
||||
Button({ class: 'btn btn-circle btn-primary', onclick: () => $count(c => c + 1) }, "+")
|
||||
]);
|
||||
};
|
||||
Mount(Counter, '#demo-counter');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Derived State (Computed)
|
||||
Signals can depend on other signals. The "Double" value updates automatically.
|
||||
|
||||
<div class="card bg-base-200 border border-base-300 shadow-sm my-6">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-sm uppercase opacity-50 mb-4">Live Demo</h3>
|
||||
<div id="demo-computed" class="bg-base-100 p-6 rounded-xl border border-base-300"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
```javascript
|
||||
const ComputedDemo = () => {
|
||||
const $count = $(10);
|
||||
const $double = $(() => $count() * 2);
|
||||
return Div({ class: 'space-y-4' }, [
|
||||
Input({ type: 'range', min: 1, max: 100, class: 'range range-primary', value: $count }),
|
||||
P({ class: 'text-center' }, ["Base: ", $count, " ⮕ ", Span({class: 'text-primary font-bold'}, ["Double: ", $double])])
|
||||
]);
|
||||
};
|
||||
Mount(ComputedDemo, '#demo-computed');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Lists and Loops (For)
|
||||
SigPro handles lists efficiently, only updating specific DOM nodes.
|
||||
|
||||
<div class="card bg-base-200 border border-base-300 shadow-sm my-6">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-sm uppercase opacity-50 mb-4">Live Demo</h3>
|
||||
<div id="demo-list" class="bg-base-100 p-6 rounded-xl border border-base-300"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
```javascript
|
||||
const ListDemo = () => {
|
||||
const $todos = $(['Learn SigPro', 'Build an App']);
|
||||
const $input = $("");
|
||||
const addTodo = () => {
|
||||
if ($input()) {
|
||||
$todos(prev => [...prev, $input()]);
|
||||
$input("");
|
||||
}
|
||||
};
|
||||
return Div([
|
||||
Div({ class: 'flex gap-2 mb-4' }, [
|
||||
Input({ class: 'input input-bordered flex-1', value: $input, placeholder: 'New task...' }),
|
||||
Button({ class: 'btn btn-primary', onclick: addTodo }, "Add")
|
||||
]),
|
||||
Ul({ class: 'menu bg-base-200 rounded-box p-2' },
|
||||
For($todos, (item) => Li([A(item)]), (item) => item)
|
||||
)
|
||||
]);
|
||||
};
|
||||
Mount(ListDemo, '#demo-list');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Conditional Rendering (If)
|
||||
Toggle visibility without re-rendering the entire parent.
|
||||
|
||||
<div class="card bg-base-200 border border-base-300 shadow-sm my-6">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-sm uppercase opacity-50 mb-4">Live Demo</h3>
|
||||
<div id="demo-if" class="bg-base-100 p-6 rounded-xl border border-base-300 flex justify-center"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
```javascript
|
||||
const ConditionalDemo = () => {
|
||||
const $isVisible = $(false);
|
||||
return Div({ class: 'text-center w-full' }, [
|
||||
Button({ class: 'btn btn-outline mb-4', onclick: () => $isVisible(! $isVisible()) }, "Toggle Secret"),
|
||||
If($isVisible,
|
||||
() => Div({ class: 'p-4 bg-warning text-warning-content rounded-lg shadow-inner' }, "🤫 SigPro is Awesome!"),
|
||||
() => Div({ class: 'p-4 opacity-50' }, "Nothing to see here...")
|
||||
)
|
||||
]);
|
||||
};
|
||||
Mount(ConditionalDemo, '#demo-if');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Persistent State
|
||||
Pass a string as a second argument to `$(val, key)` to sync with `localStorage`.
|
||||
|
||||
<div class="card bg-base-200 border border-base-300 shadow-sm my-6">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-sm uppercase opacity-50 mb-4">Live Demo</h3>
|
||||
<div id="demo-persist" class="bg-base-100 p-6 rounded-xl border border-base-300"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
```javascript
|
||||
const PersistDemo = () => {
|
||||
const $name = $("Guest", "sigpro-demo-name");
|
||||
return Div({ class: 'flex flex-col gap-2' }, [
|
||||
H3({ class: 'text-lg font-bold' }, ["Hello, ", $name]),
|
||||
Input({ class: 'input input-bordered', value: $name, placeholder: 'Type your name...' }),
|
||||
P({ class: 'text-xs opacity-50' }, "Refresh the page to see magic!")
|
||||
]);
|
||||
};
|
||||
Mount(PersistDemo, '#demo-persist');
|
||||
```
|
||||
@@ -37,19 +37,25 @@
|
||||
successText:
|
||||
'<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><polyline points="20 6 9 17 4 12"></polyline></svg>',
|
||||
},
|
||||
search: {
|
||||
placeholder: "Type to search",
|
||||
noData: "No Results!",
|
||||
depth: 3,
|
||||
hideOtherSidebarContent: true,
|
||||
},
|
||||
plugins: [
|
||||
function (hook, vm) {
|
||||
hook.doneEach(function () {
|
||||
const codeBlocks = document.querySelectorAll(
|
||||
'pre[data-lang="javascript"] code',
|
||||
'pre[data-lang="js"] code',
|
||||
);
|
||||
|
||||
codeBlocks.forEach((code) => {
|
||||
try {
|
||||
const runDemo = new Function(code.innerText);
|
||||
const scriptContent = `(function() { ${code.innerText} })();`;
|
||||
const runDemo = new Function(scriptContent);
|
||||
runDemo();
|
||||
} catch (err) {
|
||||
console.error("Error en la demo de SigPro:", err);
|
||||
console.error("Error ejecutando demo de SigPro:", err);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -57,9 +63,44 @@
|
||||
],
|
||||
};
|
||||
</script>
|
||||
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/search.min.js"></script>
|
||||
<script src="./sigpro.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify/lib/docsify.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify-copy-code/dist/docsify-copy-code.min.js"></script>
|
||||
<style>
|
||||
button, input {
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
}
|
||||
button {
|
||||
background-color: #3b82f6;
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
margin: 0.25rem;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
input {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 0.375rem;
|
||||
margin: 0.25rem;
|
||||
background-color: white;
|
||||
}
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 2px rgba(59,130,246,0.2);
|
||||
}
|
||||
/* Opcional: para listas y párrafos dentro de demos */
|
||||
.example, div:has(> button) {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Installation & Setup
|
||||
# Installation & Setup (SigPro 1.2.18)
|
||||
|
||||
SigPro is designed to be drop-in ready. Whether you are building a complex application with a bundler or a simple reactive widget in a single HTML file, SigPro scales with your needs.
|
||||
|
||||
@@ -47,36 +47,20 @@ bun add sigpro
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
// Import the core and UI components
|
||||
import SigPro from "[https://cdn.jsdelivr.net/npm/sigpro@latest/+esm](https://cdn.jsdelivr.net/npm/sigpro@latest/+esm)";
|
||||
import { UI } from "[https://cdn.jsdelivr.net/npm/sigpro@latest/ui/+esm](https://cdn.jsdelivr.net/npm/sigpro@latest/ui/+esm)";
|
||||
|
||||
// Initialize UI components globally
|
||||
UI($);
|
||||
// Import the core – it auto-installs itself globally
|
||||
import 'https://cdn.jsdelivr.net/npm/sigpro@1.2.18/+esm';
|
||||
// Now $, $$, watch, h, when, each, fx, router, req, mount, batch and all lowercase tag helpers (div, button, etc.) are available
|
||||
</script>
|
||||
```
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="radio" name="install_method" class="tab border-base-300 whitespace-nowrap" aria-label="CDN (ESM)" />
|
||||
<div class="tab-content bg-base-100 border-base-300 rounded-box p-6">
|
||||
<pre class="bg-base-200 p-4 rounded-lg"><code class="language-html"><script type="module">
|
||||
// Import the core and UI components
|
||||
import SigPro from 'https://cdn.jsdelivr.net/npm/sigpro@latest/+esm';
|
||||
import { UI } from 'https://cdn.jsdelivr.net/npm/sigpro@latest/ui/+esm';
|
||||
|
||||
// Initialize UI components globally
|
||||
UI($);
|
||||
</script></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 2. Quick Start Examples
|
||||
|
||||
SigPro uses **PascalCase** for Tag Helpers (e.g., `Div`, `Button`) to provide a clean, component-like syntax without needing JSX.
|
||||
SigPro uses **lowercase** Tag Helpers (e.g., `div`, `button`) to keep the syntax close to raw HTML, while still being pure JavaScript functions.
|
||||
|
||||
<div class="tabs tabs-box w-full mt-8 mb-12 bg-base-200/50 p-2 rounded-xl border border-base-300">
|
||||
<input type="radio" name="quick_start_tabs" class="tab !rounded-lg" aria-label="Mainstream (Bundlers)" checked />
|
||||
@@ -84,29 +68,29 @@ SigPro uses **PascalCase** for Tag Helpers (e.g., `Div`, `Button`) to provide a
|
||||
|
||||
```javascript
|
||||
// File: App.js
|
||||
import "sigpro";
|
||||
import 'sigpro'; // auto-installs globals
|
||||
|
||||
export const App = () => {
|
||||
const $count = $(0);
|
||||
const count = $(0);
|
||||
|
||||
// Tag Helpers like Div, H1, Button are available globally
|
||||
return Div({ class: "card p-4" }, [
|
||||
H1(["Count is: ", $count]),
|
||||
Button(
|
||||
// Tag helpers like div, h1, button are available globally (lowercase)
|
||||
return div({ class: "card p-4" }, [
|
||||
h1(() => `Count is: ${count()}`),
|
||||
button(
|
||||
{
|
||||
class: "btn btn-primary",
|
||||
onclick: () => $count((c) => c + 1),
|
||||
onclick: () => count(count() + 1),
|
||||
},
|
||||
"Increment",
|
||||
"Increment"
|
||||
),
|
||||
]);
|
||||
};
|
||||
|
||||
// File: main.js
|
||||
import "sigpro";
|
||||
import { App } from "./App.js";
|
||||
import 'sigpro';
|
||||
import { App } from './App.js';
|
||||
|
||||
Mount(App, "#app");
|
||||
mount(App, '#app');
|
||||
```
|
||||
|
||||
</div>
|
||||
@@ -121,23 +105,23 @@ Mount(App, "#app");
|
||||
<div id="app"></div>
|
||||
|
||||
<script type="module">
|
||||
import SigPro from "https://cdn.jsdelivr.net/npm/sigpro@latest/+esm";
|
||||
import 'https://cdn.jsdelivr.net/npm/sigpro@1.2.18/+esm';
|
||||
|
||||
const $name = $("Developer");
|
||||
const name = $('Developer');
|
||||
|
||||
// No need to import Div, Section, H2, Input... they are global!
|
||||
// Lowercase tag helpers: section, h2, input
|
||||
const App = () =>
|
||||
Section({ class: "container" }, [
|
||||
H2(["Welcome, ", $name]),
|
||||
Input({
|
||||
section({ class: "container" }, [
|
||||
h2(() => `Welcome, ${name()}`),
|
||||
input({
|
||||
type: "text",
|
||||
class: "input input-bordered",
|
||||
$value: $name, // Automatic two-way binding
|
||||
value: name, // ✅ Two-way binding: signal as value + automatic input event
|
||||
placeholder: "Type your name...",
|
||||
}),
|
||||
]);
|
||||
|
||||
Mount(App, "#app");
|
||||
mount(App, '#app');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -152,17 +136,16 @@ Mount(App, "#app");
|
||||
|
||||
One of SigPro's core strengths is its **Global API**, which eliminates "Import Hell" while remaining ESM-compatible.
|
||||
|
||||
- **The "Zero-Config" Import:** By simply adding `import SigPro from "sigpro"`, the framework automatically "hydrates" the global `window` object.
|
||||
- **Core Functions:** You get immediate access to `$`, `Watch`, `Tag`, `If`, `For`, and `Router` anywhere in your scripts without using the `SigPro.` prefix.
|
||||
- **Auto-Installation:** This happens instantly upon import thanks to its built-in `install()` routine, making it "Plug & Play" for both local projects and CDN usage.
|
||||
- **The "Zero-Config" Import:** By simply adding `import 'sigpro'` (or importing from the CDN), the framework automatically "hydrates" the global `window` object.
|
||||
- **Core Functions:** You get immediate access to `$`, `$$`, `watch`, `h`, `when`, `each`, `fx`, `router`, `req`, `mount`, `batch` anywhere in your scripts.
|
||||
- **Auto-Installation:** This happens instantly upon import thanks to its built-in self‑installation, making it "Plug & Play" for both local projects and CDN usage.
|
||||
|
||||
- **PascalCase Tag Helpers:** Standard HTML tags are pre-registered as global functions (`Div`, `Span`, `Button`, `Section`, etc.).
|
||||
- **Clean UI Syntax:** This allows you to write UI structures that look like HTML but are pure, reactive JavaScript: `Div({ class: "card" }, [ H1("Title") ])`.
|
||||
- **Lowercase Tag Helpers:** All standard HTML tags are pre-registered as global functions (`div`, `span`, `button`, `section`, `input`, `h1`, `h2`, etc.).
|
||||
- **Clean UI Syntax:** Write UI structures that look almost like HTML but are pure, reactive JavaScript: `div({ class: "card" }, [ h1("Title") ])`.
|
||||
|
||||
- **Hybrid Tree Shaking:** \* For **Maximum Speed**, use `import SigPro from "sigpro"`.
|
||||
- For **Maximum Optimization**, you can still use Named Imports: `import { $, Tag } from "sigpro"`. This allows modern bundlers like Vite to prune unused code while keeping your core reactive.
|
||||
- **Tree Shaking Friendly:** For maximum optimization, you can still use named imports: `import { $, watch, mount } from 'sigpro'`. Modern bundlers (Vite, esbuild) will prune unused code.
|
||||
|
||||
- **Custom Components:** We recommend using **PascalCase** for your own components (e.g., `UserCard()`) to maintain visual consistency with the built-in Tag Helpers and distinguish them from standard logic.
|
||||
- **Custom Components:** We recommend using **PascalCase** for your own components (e.g., `UserCard()`) to distinguish them from built-in lowercase tag helpers.
|
||||
|
||||
---
|
||||
|
||||
@@ -179,7 +162,7 @@ SigPro stands out by removing the "Build Step" tax and the "Virtual DOM" overhea
|
||||
|
||||
| Feature | **SigPro** | **SolidJS** | **Svelte** | **React** | **Vue** |
|
||||
| :----------------- | :--------------- | :----------- | :----------- | :---------- | :---------- |
|
||||
| **Bundle Size** | **~2KB** | ~7KB | ~4KB | ~40KB+ | ~30KB |
|
||||
| **Bundle Size** | **~3KB** | ~7KB | ~4KB | ~40KB+ | ~30KB |
|
||||
| **DOM Strategy** | **Direct DOM** | Direct DOM | Compiled DOM | Virtual DOM | Virtual DOM |
|
||||
| **Reactivity** | **Fine-grained** | Fine-grained | Compiled | Re-renders | Proxies |
|
||||
| **Build Step** | **Optional** | Required | Required | Required | Optional |
|
||||
@@ -190,14 +173,16 @@ SigPro stands out by removing the "Build Step" tax and the "Virtual DOM" overhea
|
||||
|
||||
## 6. Key Advantages
|
||||
|
||||
- **Extreme Performance**: No Virtual DOM reconciliation. SigPro updates the specific node or attribute instantly when a Signal changes.
|
||||
- **Extreme Performance**: No Virtual DOM reconciliation. SigPro updates the specific node or attribute instantly when a signal changes.
|
||||
- **Fine-Grained Reactivity**: State changes only trigger updates where the data is actually used, not on the entire component.
|
||||
- **Native Web Standards**: Everything is a standard JS function. No custom template syntax to learn.
|
||||
- **Zero Magic**: No hidden compilers. What you write is what runs in the browser.
|
||||
- **Global by Design**: Tag Helpers and the `$` function are available globally to eliminate "Import Hell" and keep your code clean.
|
||||
- **Global by Design**: Tag helpers and core functions are available globally to eliminate "Import Hell" and keep your code clean.
|
||||
|
||||
---
|
||||
|
||||
## 7. Summary
|
||||
|
||||
SigPro isn't just another framework; it's a bridge to the native web. By using standard ES Modules and functional DOM generation, you gain the benefits of a modern library with the weight of a utility script.
|
||||
SigPro isn't just another framework; it's a bridge to the native web. By using standard ES Modules and functional DOM generation, you get the benefits of a modern reactive library with the weight of a utility script.
|
||||
|
||||
**Because, in the end... why fight the web when we can embrace it?**
|
||||
|
||||
Reference in New Issue
Block a user