Add $$ proxies + Svg compatible + $if transaction

This commit is contained in:
2026-04-04 02:59:28 +02:00
parent 196f82f240
commit 05660fde2b
7 changed files with 715 additions and 99 deletions

View File

@@ -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<boolean> | 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); }
};
```

View File

@@ -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
$$<T extends object>(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
```