From 05660fde2b057c6c40ab7d7e9d7910bc40687bc3 Mon Sep 17 00:00:00 2001 From: natxocc Date: Sat, 4 Apr 2026 02:59:28 +0200 Subject: [PATCH] Add $$ proxies + Svg compatible + $if transaction --- docs/api/if.md | 130 +++++++++++++++++---- docs/api/signal.md | 273 +++++++++++++++++++++++++++++++++++++++++++++ docs/examples.md | 2 - docs/install.md | 4 +- package.json | 3 +- sigpro.d.ts | 261 +++++++++++++++++++++++++++++++++++++++++++ sigpro/index.js | 141 +++++++++++------------ 7 files changed, 715 insertions(+), 99 deletions(-) create mode 100644 sigpro.d.ts diff --git a/docs/api/if.md b/docs/api/if.md index ce100c8..fecb907 100644 --- a/docs/api/if.md +++ b/docs/api/if.md @@ -1,7 +1,6 @@ - # Reactive Branching: `$if( )` -The `$if` function is a reactive control flow operator. It manages the conditional rendering of components, ensuring that only the active branch exists in the DOM and in memory. +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 @@ -9,7 +8,8 @@ The `$if` function is a reactive control flow operator. It manages the condition $if( condition: Signal | Function, thenVal: Component | Node, - otherwiseVal?: Component | Node + otherwiseVal?: Component | Node, + transition?: Transition ): HTMLElement ``` @@ -18,15 +18,49 @@ $if( | **`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 -The most common use case is showing or hiding a single element based on a state. ```javascript const isVisible = $(false); @@ -41,8 +75,25 @@ Div([ ]); ``` -### 2. Lazy Component Loading -Unlike using a hidden class (CSS `display: none`), `$if` is **lazy**. The branch that isn't active **is never created**. This saves memory and initial processing time. +### 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(), @@ -51,29 +102,41 @@ $if(() => user.isLogged(), ) ``` +### 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 `$html` are removed. -3. **Recursive Sweep**: SigPro performs a deep "sweep" of the removed branch to ensure no nested reactive effects remain active. - - +1. **Stop Watchers**: All `$watch` calls inside the inactive branch are permanently stopped. +2. **Unbind Events**: Event listeners attached via `$html` 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**: If your branches are heavy (e.g., they contain complex components), wrap them in a function `() => MyComponent()`. This prevents the component from being initialized until the condition actually meets its requirement. -* **Logical Expressions**: You can pass a complex computed function as the condition: - ```javascript - $if(() => count() > 10 && status() === 'ready', - Span("Threshold reached!") - ) - ``` +- **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. --- @@ -82,7 +145,36 @@ One of the core strengths of `$if` is its integrated **Cleanup** logic. SigPro e | Feature | Standard CSS `hidden` | SigPro `$if` | | :--- | :--- | :--- | | **DOM Presence** | Always present | Only if active | -| **Reactivity** | Still processing in background | **Paused/Destroyed** | +| **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); } +}; +``` \ No newline at end of file diff --git a/docs/api/signal.md b/docs/api/signal.md index a4a5aba..12b44ea 100644 --- a/docs/api/signal.md +++ b/docs/api/signal.md @@ -66,3 +66,276 @@ const list = $(["A", "B"]); // Adds "C" using the previous state list(prev => [...prev, "C"]); ``` + +--- + +# The Reactive Object: `$$( )` + +The `$$( )` function creates a reactive proxy for complex nested objects. Unlike `$()`, which tracks a single value, `$$()` tracks **every property access** automatically. + +## Function Signature + +```typescript +$$(obj: T): T +``` + +| Parameter | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **`obj`** | `object` | Yes | The object to make reactive. Properties are tracked recursively. | + +**Returns:** A reactive proxy that behaves like the original object but triggers updates when any property changes. + +--- + +## Usage Patterns + +### 1. Simple Object + +```javascript +const state = $$({ count: 0, name: "Juan" }); + +$watch(() => state.count, () => { + console.log(`Count is now ${state.count}`); +}); + +state.count++; // ✅ Triggers update +state.name = "Ana"; // ✅ Also reactive +``` + +### 2. Deep Reactivity + +Unlike `$()`, `$$()` tracks nested properties automatically. + +```javascript +const user = $$({ + profile: { + name: "Juan", + address: { + city: "Madrid", + zip: "28001" + } + } +}); + +// This works! Tracks deep property access +$watch(() => user.profile.address.city, () => { + console.log("City changed"); +}); + +user.profile.address.city = "Barcelona"; // ✅ Triggers update +``` + +### 3. Arrays + +`$$()` works with arrays and array methods. + +```javascript +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`); +}); + +// 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 +``` + +### 4. Mixed with Signals + +`$$()` works seamlessly with `$()` signals. + +```javascript +const form = $$({ + fields: { + email: "", + password: "" + }, + isValid: $(false) // Signal inside reactive object +}); + +// 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 +}); +``` + +--- + +## Key Differences: `$()` vs `$$()` + +| Feature | `$()` Signal | `$$()` Reactive | +| :--- | :--- | :--- | +| **Primitives** | ✅ Works directly | ❌ Needs wrapper object | +| **Objects** | Manual tracking | ✅ Automatic deep tracking | +| **Nested properties** | ❌ Not reactive | ✅ Fully reactive | +| **Arrays** | Requires reassignment | ✅ Methods (push, pop, etc.) work | +| **Syntax** | `count()` / `count(5)` | `state.count = 5` | +| **LocalStorage** | ✅ Built-in | ❌ (use `$()` for persistence) | +| **Performance** | Lighter | Slightly heavier (Proxy) | +| **Destructuring** | ✅ Safe | ❌ Breaks reactivity | + +--- + +## When to Use Each + +### Use `$()` when: +- Working with primitives (numbers, strings, booleans) +- Need localStorage persistence +- Creating computed values +- Want explicit control over updates + +```javascript +const count = $(0); +const user = $(null); +const fullName = $(() => `${firstName()} ${lastName()}`); +``` + +### 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 +const form = $$({ email: "", password: "" }); +const settings = $$({ theme: "dark", notifications: true }); +const store = $$({ users: [], filters: {}, pagination: { page: 1 } }); +``` + +--- + +## Important Notes + +### ✅ DO: +```javascript +// Access properties directly +state.count = 10; +state.user.name = "Ana"; +todos.push(newItem); + +// Track in effects +$watch(() => state.count, () => {}); +$watch(() => state.user.name, () => {}); +``` + +### ❌ DON'T: +```javascript +// Destructuring breaks reactivity +const { count, user } = state; // ❌ count and user are not reactive + +// Reassigning the whole object +state = { count: 10 }; // ❌ Loses reactivity + +// Using primitive directly +const count = $$(0); // ❌ Doesn't work (use $() instead) +``` + +--- + +## Automatic Cleanup + +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` + +--- + +## Technical Comparison + +| Aspect | `$()` | `$$()` | +| :--- | :--- | :--- | +| **Implementation** | Closure with Set | Proxy with WeakMap | +| **Tracking** | Explicit (function call) | Implicit (property access) | +| **Memory** | Minimal | Slightly more (WeakMap cache) | +| **Use Case** | Simple state | Complex state | +| **Learning Curve** | Low | Low (feels like plain JS) | + +--- + +## Complete Example + +```javascript +// Combining both approaches +const app = { + // Simple primitives with persistence + theme: $("dark", "theme"), + sidebarOpen: $(true), + + // Complex state with $$() + user: $$({ + name: "", + email: "", + preferences: { + notifications: true, + language: "es" + } + }), + + // Computed values + isLoggedIn: $(() => !!app.user.name), + + // Actions + login(name, email) { + app.user.name = name; + app.user.email = email; + }, + + logout() { + app.user.name = ""; + app.user.email = ""; + app.user.preferences.notifications = true; + } +}; + +// 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() + ) + ]); +}; +``` + +--- + +## Migration from `$()` to `$$()` + +If you have code using nested signals: + +```javascript +// Before - Manual nesting +const user = $({ + name: $(""), + email: $("") +}); +user().name("Juan"); // Need to call inner signal + +// After - Automatic nesting +const user = $$({ + name: "", + email: "" +}); +user.name = "Juan"; // Direct assignment +``` \ No newline at end of file diff --git a/docs/examples.md b/docs/examples.md index f9165a5..c218f3c 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -137,11 +137,9 @@ $mount(PersistDemo, '#demo-persist'); ```