Add $$ proxies + Svg compatible + $if transaction
This commit is contained in:
126
docs/api/if.md
126
docs/api/if.md
@@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
# Reactive Branching: `$if( )`
|
# 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
|
## Function Signature
|
||||||
|
|
||||||
@@ -9,7 +8,8 @@ The `$if` function is a reactive control flow operator. It manages the condition
|
|||||||
$if(
|
$if(
|
||||||
condition: Signal<boolean> | Function,
|
condition: Signal<boolean> | Function,
|
||||||
thenVal: Component | Node,
|
thenVal: Component | Node,
|
||||||
otherwiseVal?: Component | Node
|
otherwiseVal?: Component | Node,
|
||||||
|
transition?: Transition
|
||||||
): HTMLElement
|
): HTMLElement
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -18,15 +18,49 @@ $if(
|
|||||||
| **`condition`** | `Signal` | Yes | A reactive source that determines which branch to render. |
|
| **`condition`** | `Signal` | Yes | A reactive source that determines which branch to render. |
|
||||||
| **`thenVal`** | `any` | Yes | The content to show when the condition is **truthy**. |
|
| **`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). |
|
| **`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.
|
**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
|
## Usage Patterns
|
||||||
|
|
||||||
### 1. Simple Toggle
|
### 1. Simple Toggle
|
||||||
The most common use case is showing or hiding a single element based on a state.
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const isVisible = $(false);
|
const isVisible = $(false);
|
||||||
@@ -41,8 +75,25 @@ Div([
|
|||||||
]);
|
]);
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Lazy Component Loading
|
### 2. With Smooth Animation
|
||||||
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.
|
|
||||||
|
```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
|
```javascript
|
||||||
$if(() => user.isLogged(),
|
$if(() => user.isLogged(),
|
||||||
@@ -51,6 +102,23 @@ $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
|
## Automatic Cleanup
|
||||||
@@ -59,21 +127,16 @@ One of the core strengths of `$if` is its integrated **Cleanup** logic. SigPro e
|
|||||||
|
|
||||||
1. **Stop Watchers**: All `$watch` calls inside the inactive branch are permanently stopped.
|
1. **Stop Watchers**: All `$watch` calls inside the inactive branch are permanently stopped.
|
||||||
2. **Unbind Events**: Event listeners attached via `$html` are removed.
|
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.
|
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
|
## 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.
|
- **Function Wrappers**: For heavy components, use `() => MyComponent()` to prevent initialization until needed.
|
||||||
* **Logical Expressions**: You can pass a complex computed function as the condition:
|
- **Reusable Transitions**: Define common transitions (fade, slide, scale) in a shared module.
|
||||||
```javascript
|
- **Cleanup**: No manual cleanup needed. SigPro handles everything automatically.
|
||||||
$if(() => count() > 10 && status() === 'ready',
|
|
||||||
Span("Threshold reached!")
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -82,7 +145,36 @@ One of the core strengths of `$if` is its integrated **Cleanup** logic. SigPro e
|
|||||||
| Feature | Standard CSS `hidden` | SigPro `$if` |
|
| Feature | Standard CSS `hidden` | SigPro `$if` |
|
||||||
| :--- | :--- | :--- |
|
| :--- | :--- | :--- |
|
||||||
| **DOM Presence** | Always present | Only if active |
|
| **DOM Presence** | Always present | Only if active |
|
||||||
| **Reactivity** | Still processing in background | **Paused/Destroyed** |
|
| **Reactivity** | Still processing | **Paused/Destroyed** |
|
||||||
| **Memory usage** | Higher | **Optimized** |
|
| **Memory usage** | Higher | **Optimized** |
|
||||||
| **Cleanup** | Manual | **Automatic** |
|
| **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); }
|
||||||
|
};
|
||||||
|
```
|
||||||
@@ -66,3 +66,276 @@ const list = $(["A", "B"]);
|
|||||||
// Adds "C" using the previous state
|
// Adds "C" using the previous state
|
||||||
list(prev => [...prev, "C"]);
|
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
|
||||||
|
```
|
||||||
@@ -137,11 +137,9 @@ $mount(PersistDemo, '#demo-persist');
|
|||||||
```
|
```
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Esta función envuelve todo para que Docsify lo ejecute correctamente
|
|
||||||
(function() {
|
(function() {
|
||||||
const initExamples = () => {
|
const initExamples = () => {
|
||||||
|
|
||||||
// 1. Counter
|
|
||||||
const counterTarget = document.querySelector('#demo-counter');
|
const counterTarget = document.querySelector('#demo-counter');
|
||||||
if (counterTarget && !counterTarget.hasChildNodes()) {
|
if (counterTarget && !counterTarget.hasChildNodes()) {
|
||||||
const Counter = () => {
|
const Counter = () => {
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ SigPro uses **PascalCase** for Tag Helpers (e.g., `Div`, `Button`) to provide a
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// File: App.js
|
// File: App.js
|
||||||
import SigPro from "sigpro";
|
import "sigpro";
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
const $count = $(0);
|
const $count = $(0);
|
||||||
@@ -104,7 +104,7 @@ export const App = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// File: main.js
|
// File: main.js
|
||||||
import SigPro from "sigpro";
|
import "sigpro";
|
||||||
import { App } from "./App.js";
|
import { App } from "./App.js";
|
||||||
|
|
||||||
$mount(App, "#app");
|
$mount(App, "#app");
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"import": "./index.js",
|
"import": "./index.js",
|
||||||
"script": "./dist/sigpro.js"
|
"script": "./dist/sigpro.js",
|
||||||
|
"types": "./sigpro/sigpro.d.ts"
|
||||||
},
|
},
|
||||||
"./vite": "./vite/index.js",
|
"./vite": "./vite/index.js",
|
||||||
"./vite/*": "./vite/*.js"
|
"./vite/*": "./vite/*.js"
|
||||||
|
|||||||
261
sigpro.d.ts
vendored
Normal file
261
sigpro.d.ts
vendored
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
// sigpro.d.ts
|
||||||
|
|
||||||
|
export declare module 'sigpro' {
|
||||||
|
type Signal<T> = {
|
||||||
|
(): T;
|
||||||
|
(value: T | ((prev: T) => T)): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ComputedSignal<T> = {
|
||||||
|
(): T;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ReactiveObject<T extends object> = T;
|
||||||
|
|
||||||
|
type DeepReactive<T> = T extends object
|
||||||
|
? { [K in keyof T]: DeepReactive<T[K]> }
|
||||||
|
: T;
|
||||||
|
|
||||||
|
type Unsubscribe = () => void;
|
||||||
|
|
||||||
|
type CleanupFn = () => void;
|
||||||
|
|
||||||
|
type ViewInstance = {
|
||||||
|
_isRuntime: true;
|
||||||
|
container: HTMLDivElement;
|
||||||
|
destroy: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ComponentFunction<P = {}> = (props?: P) => HTMLElement | ViewInstance | string | null;
|
||||||
|
type ComponentChild = HTMLElement | ViewInstance | string | number | boolean | null | undefined;
|
||||||
|
type ComponentChildren = ComponentChild | ComponentChild[];
|
||||||
|
|
||||||
|
interface BaseProps {
|
||||||
|
class?: string | (() => string);
|
||||||
|
style?: string | Record<string, string> | (() => string | Record<string, string>);
|
||||||
|
id?: string | (() => string);
|
||||||
|
ref?: ((el: HTMLElement) => void) | { current: HTMLElement | null };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EventProps {
|
||||||
|
onclick?: (event: MouseEvent) => void;
|
||||||
|
oninput?: (event: Event) => void;
|
||||||
|
onchange?: (event: Event) => void;
|
||||||
|
onblur?: (event: FocusEvent) => void;
|
||||||
|
onfocus?: (event: FocusEvent) => void;
|
||||||
|
onkeydown?: (event: KeyboardEvent) => void;
|
||||||
|
onkeyup?: (event: KeyboardEvent) => void;
|
||||||
|
onmouseenter?: (event: MouseEvent) => void;
|
||||||
|
onmouseleave?: (event: MouseEvent) => void;
|
||||||
|
onsubmit?: (event: Event) => void;
|
||||||
|
[key: `on${string}`]: ((event: any) => void) | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HtmlProps extends BaseProps, EventProps {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Route {
|
||||||
|
path: string;
|
||||||
|
component: ComponentFunction | (() => Promise<ComponentFunction>);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RouterOutlet extends HTMLElement {
|
||||||
|
params: Signal<Record<string, string>>;
|
||||||
|
to: (path: string) => void;
|
||||||
|
back: () => void;
|
||||||
|
path: () => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function $<T>(initial: T, key?: string): Signal<T>;
|
||||||
|
function $<T>(computed: () => T): ComputedSignal<T>;
|
||||||
|
function $$<T extends object>(obj: T): T;
|
||||||
|
|
||||||
|
function $watch(
|
||||||
|
target: () => any,
|
||||||
|
fn?: never
|
||||||
|
): Unsubscribe;
|
||||||
|
function $watch(
|
||||||
|
target: Array<() => any>,
|
||||||
|
fn: () => void
|
||||||
|
): Unsubscribe;
|
||||||
|
|
||||||
|
function $html(
|
||||||
|
tag: string,
|
||||||
|
props?: HtmlProps | ComponentChildren,
|
||||||
|
content?: ComponentChildren
|
||||||
|
): HTMLElement;
|
||||||
|
|
||||||
|
interface Transition {
|
||||||
|
in: (el: HTMLElement) => void;
|
||||||
|
out: (el: HTMLElement, done: () => void) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function $if(
|
||||||
|
condition: boolean | (() => boolean),
|
||||||
|
thenVal: ComponentFunction | ComponentChild,
|
||||||
|
otherwiseVal?: ComponentFunction | ComponentChild,
|
||||||
|
transition?: Transition
|
||||||
|
): HTMLDivElement;
|
||||||
|
|
||||||
|
namespace $if {
|
||||||
|
function not(
|
||||||
|
condition: boolean | (() => boolean),
|
||||||
|
thenVal: ComponentFunction | ComponentChild,
|
||||||
|
otherwiseVal?: ComponentFunction | ComponentChild
|
||||||
|
): HTMLDivElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
function $for<T>(
|
||||||
|
source: T[] | (() => T[]),
|
||||||
|
render: (item: T, index: number) => ComponentChild,
|
||||||
|
keyFn?: (item: T, index: number) => string | number,
|
||||||
|
tag?: string,
|
||||||
|
props?: HtmlProps
|
||||||
|
): HTMLElement;
|
||||||
|
|
||||||
|
function $router(routes: Route[]): RouterOutlet;
|
||||||
|
|
||||||
|
function $mount(
|
||||||
|
component: ComponentFunction | HTMLElement,
|
||||||
|
target: string | HTMLElement
|
||||||
|
): ViewInstance | undefined;
|
||||||
|
|
||||||
|
const Div: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const Span: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const P: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const H1: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const H2: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const H3: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const H4: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const H5: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const H6: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const Button: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const Input: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const Textarea: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const Select: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const Option: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const Label: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const Form: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const A: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const Img: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const Ul: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const Ol: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const Li: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const Nav: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const Header: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const Footer: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const Section: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const Article: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const Aside: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const Main: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const Table: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const Thead: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const Tbody: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const Tr: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const Th: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
const Td: (props?: HtmlProps | ComponentChildren, content?: ComponentChildren) => HTMLElement;
|
||||||
|
|
||||||
|
const SigPro: {
|
||||||
|
$: typeof $;
|
||||||
|
$$: typeof $$;
|
||||||
|
$watch: typeof $watch;
|
||||||
|
$html: typeof $html;
|
||||||
|
$if: typeof $if;
|
||||||
|
$for: typeof $for;
|
||||||
|
$router: typeof $router;
|
||||||
|
$mount: typeof $mount;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare global {
|
||||||
|
const $: typeof import('sigpro').$;
|
||||||
|
const $$: typeof import('sigpro').$$;
|
||||||
|
const $watch: typeof import('sigpro').$watch;
|
||||||
|
const $html: typeof import('sigpro').$html;
|
||||||
|
const $if: typeof import('sigpro').$if;
|
||||||
|
const $for: typeof import('sigpro').$for;
|
||||||
|
const $router: typeof import('sigpro').$router;
|
||||||
|
const $mount: typeof import('sigpro').$mount;
|
||||||
|
|
||||||
|
const Div: typeof import('sigpro').Div;
|
||||||
|
const Span: typeof import('sigpro').Span;
|
||||||
|
const P: typeof import('sigpro').P;
|
||||||
|
const H1: typeof import('sigpro').H1;
|
||||||
|
const H2: typeof import('sigpro').H2;
|
||||||
|
const H3: typeof import('sigpro').H3;
|
||||||
|
const H4: typeof import('sigpro').H4;
|
||||||
|
const H5: typeof import('sigpro').H5;
|
||||||
|
const H6: typeof import('sigpro').H6;
|
||||||
|
const Button: typeof import('sigpro').Button;
|
||||||
|
const Input: typeof import('sigpro').Input;
|
||||||
|
const Textarea: typeof import('sigpro').Textarea;
|
||||||
|
const Select: typeof import('sigpro').Select;
|
||||||
|
const Option: typeof import('sigpro').Option;
|
||||||
|
const Label: typeof import('sigpro').Label;
|
||||||
|
const Form: typeof import('sigpro').Form;
|
||||||
|
const A: typeof import('sigpro').A;
|
||||||
|
const Img: typeof import('sigpro').Img;
|
||||||
|
const Ul: typeof import('sigpro').Ul;
|
||||||
|
const Ol: typeof import('sigpro').Ol;
|
||||||
|
const Li: typeof import('sigpro').Li;
|
||||||
|
const Nav: typeof import('sigpro').Nav;
|
||||||
|
const Header: typeof import('sigpro').Header;
|
||||||
|
const Footer: typeof import('sigpro').Footer;
|
||||||
|
const Section: typeof import('sigpro').Section;
|
||||||
|
const Article: typeof import('sigpro').Article;
|
||||||
|
const Aside: typeof import('sigpro').Aside;
|
||||||
|
const Main: typeof import('sigpro').Main;
|
||||||
|
const Table: typeof import('sigpro').Table;
|
||||||
|
const Thead: typeof import('sigpro').Thead;
|
||||||
|
const Tbody: typeof import('sigpro').Tbody;
|
||||||
|
const Tr: typeof import('sigpro').Tr;
|
||||||
|
const Th: typeof import('sigpro').Th;
|
||||||
|
const Td: typeof import('sigpro').Td;
|
||||||
|
|
||||||
|
interface Window {
|
||||||
|
$: typeof $;
|
||||||
|
$$: typeof $$;
|
||||||
|
$watch: typeof $watch;
|
||||||
|
$html: typeof $html;
|
||||||
|
$if: typeof $if;
|
||||||
|
$for: typeof $for;
|
||||||
|
$router: typeof $router;
|
||||||
|
$mount: typeof $mount;
|
||||||
|
SigPro: typeof import('sigpro').default;
|
||||||
|
Div: typeof Div;
|
||||||
|
Span: typeof Span;
|
||||||
|
P: typeof P;
|
||||||
|
H1: typeof H1;
|
||||||
|
H2: typeof H2;
|
||||||
|
H3: typeof H3;
|
||||||
|
H4: typeof H4;
|
||||||
|
H5: typeof H5;
|
||||||
|
H6: typeof H6;
|
||||||
|
Button: typeof Button;
|
||||||
|
Input: typeof Input;
|
||||||
|
Textarea: typeof Textarea;
|
||||||
|
Select: typeof Select;
|
||||||
|
Option: typeof Option;
|
||||||
|
Label: typeof Label;
|
||||||
|
Form: typeof Form;
|
||||||
|
A: typeof A;
|
||||||
|
Img: typeof Img;
|
||||||
|
Ul: typeof Ul;
|
||||||
|
Ol: typeof Ol;
|
||||||
|
Li: typeof Li;
|
||||||
|
Nav: typeof Nav;
|
||||||
|
Header: typeof Header;
|
||||||
|
Footer: typeof Footer;
|
||||||
|
Section: typeof Section;
|
||||||
|
Article: typeof Article;
|
||||||
|
Aside: typeof Aside;
|
||||||
|
Main: typeof Main;
|
||||||
|
Table: typeof Table;
|
||||||
|
Thead: typeof Thead;
|
||||||
|
Tbody: typeof Tbody;
|
||||||
|
Tr: typeof Tr;
|
||||||
|
Th: typeof Th;
|
||||||
|
Td: typeof Td;
|
||||||
|
}
|
||||||
|
}
|
||||||
137
sigpro/index.js
137
sigpro/index.js
@@ -7,7 +7,6 @@ const effectQueue = new Set();
|
|||||||
let isFlushing = false;
|
let isFlushing = false;
|
||||||
const MOUNTED_NODES = new WeakMap();
|
const MOUNTED_NODES = new WeakMap();
|
||||||
|
|
||||||
/** flush */
|
|
||||||
const flush = () => {
|
const flush = () => {
|
||||||
if (isFlushing) return;
|
if (isFlushing) return;
|
||||||
isFlushing = true;
|
isFlushing = true;
|
||||||
@@ -19,7 +18,6 @@ const flush = () => {
|
|||||||
isFlushing = false;
|
isFlushing = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** track */
|
|
||||||
const track = (subs) => {
|
const track = (subs) => {
|
||||||
if (activeEffect && !activeEffect._deleted) {
|
if (activeEffect && !activeEffect._deleted) {
|
||||||
subs.add(activeEffect);
|
subs.add(activeEffect);
|
||||||
@@ -27,7 +25,6 @@ const track = (subs) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/** trigger */
|
|
||||||
const trigger = (subs) => {
|
const trigger = (subs) => {
|
||||||
for (const eff of subs) {
|
for (const eff of subs) {
|
||||||
if (eff === activeEffect || eff._deleted) continue;
|
if (eff === activeEffect || eff._deleted) continue;
|
||||||
@@ -41,7 +38,6 @@ const trigger = (subs) => {
|
|||||||
if (!isFlushing) queueMicrotask(flush);
|
if (!isFlushing) queueMicrotask(flush);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** sweep */
|
|
||||||
const sweep = (node) => {
|
const sweep = (node) => {
|
||||||
if (node._cleanups) {
|
if (node._cleanups) {
|
||||||
node._cleanups.forEach((f) => f());
|
node._cleanups.forEach((f) => f());
|
||||||
@@ -50,7 +46,6 @@ const sweep = (node) => {
|
|||||||
node.childNodes?.forEach(sweep);
|
node.childNodes?.forEach(sweep);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** _view */
|
|
||||||
const _view = (fn) => {
|
const _view = (fn) => {
|
||||||
const cleanups = new Set();
|
const cleanups = new Set();
|
||||||
const prev = currentOwner;
|
const prev = currentOwner;
|
||||||
@@ -80,17 +75,6 @@ const _view = (fn) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a reactive Signal or a Computed Value.
|
|
||||||
* @param {any|Function} initial - Initial value or a getter function for computed state.
|
|
||||||
* @param {string} [key] - Optional. Key for automatic persistence in localStorage.
|
|
||||||
* @returns {Function} Signal getter/setter. Use `sig()` to read and `sig(val)` to write.
|
|
||||||
* @example
|
|
||||||
* const count = $(0); // Simple signal
|
|
||||||
* const double = $(() => count() * 2); // Computed signal
|
|
||||||
* const name = $("John", "user-name"); // Persisted signal
|
|
||||||
*/
|
|
||||||
|
|
||||||
const $ = (initial, key = null) => {
|
const $ = (initial, key = null) => {
|
||||||
if (typeof initial === "function") {
|
if (typeof initial === "function") {
|
||||||
const subs = new Set();
|
const subs = new Set();
|
||||||
@@ -148,16 +132,39 @@ const $ = (initial, key = null) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
const $$ = (obj, cache = new WeakMap()) => {
|
||||||
* Watches for signal changes and executes a side effect.
|
if (typeof obj !== "object" || obj === null) return obj;
|
||||||
* Handles automatic cleanup of previous effects.
|
if (cache.has(obj)) return cache.get(obj);
|
||||||
* @param {Function|Array} target - Function to execute or Array of signals for explicit dependency tracking.
|
|
||||||
* @param {Function} [fn] - If the first parameter is an Array, this is the callback function.
|
const subs = {};
|
||||||
* @returns {Function} Function to manually stop the watcher.
|
|
||||||
* @example
|
const proxy = new Proxy(obj, {
|
||||||
* $watch(() => console.log("Count is:", count()));
|
get(target, key) {
|
||||||
* $watch([count], () => console.log("Only runs when count changes"));
|
if (activeEffect)
|
||||||
*/
|
track(subs[key] ??= new Set());
|
||||||
|
|
||||||
|
const value = Reflect.get(target, key);
|
||||||
|
|
||||||
|
return (typeof value === "object" && value !== null)
|
||||||
|
? $$(value, cache)
|
||||||
|
: value;
|
||||||
|
},
|
||||||
|
|
||||||
|
set(target, key, value) {
|
||||||
|
if (Object.is(target[key], value)) return true;
|
||||||
|
|
||||||
|
const res = Reflect.set(target, key, value);
|
||||||
|
|
||||||
|
if (subs[key])
|
||||||
|
trigger(subs[key]);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cache.set(obj, proxy);
|
||||||
|
return proxy;
|
||||||
|
};
|
||||||
|
|
||||||
const $watch = (target, fn) => {
|
const $watch = (target, fn) => {
|
||||||
const isExplicit = Array.isArray(target);
|
const isExplicit = Array.isArray(target);
|
||||||
@@ -212,18 +219,18 @@ const $watch = (target, fn) => {
|
|||||||
return runner.stop;
|
return runner.stop;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* DOM element rendering engine with built-in reactivity.
|
|
||||||
* @param {string} tag - HTML tag name (e.g., 'div', 'span').
|
|
||||||
* @param {Object} [props] - Attributes, events (onEvent), or two-way bindings (value, checked).
|
|
||||||
* @param {Array|any} [content] - Children: text, other nodes, or reactive signals.
|
|
||||||
* @returns {HTMLElement} The configured reactive DOM element.
|
|
||||||
*/
|
|
||||||
const $html = (tag, props = {}, content = []) => {
|
const $html = (tag, props = {}, content = []) => {
|
||||||
if (props instanceof Node || Array.isArray(props) || typeof props !== "object") {
|
if (props instanceof Node || Array.isArray(props) || typeof props !== "object") {
|
||||||
content = props; props = {};
|
content = props; props = {};
|
||||||
}
|
}
|
||||||
const el = document.createElement(tag), _sanitize = (key, val) => (key === 'src' || key === 'href') && String(val).toLowerCase().includes('javascript:') ? '#' : val;
|
|
||||||
|
const svgTags = ["svg","path","circle","rect","line","polyline","polygon","g","defs","text","tspan","use"];
|
||||||
|
const isSVG = svgTags.includes(tag);
|
||||||
|
const el = isSVG
|
||||||
|
? document.createElementNS("http://www.w3.org/2000/svg", tag)
|
||||||
|
: document.createElement(tag);
|
||||||
|
|
||||||
|
const _sanitize = (key, val) => (key === 'src' || key === 'href') && String(val).toLowerCase().includes('javascript:') ? '#' : val;
|
||||||
el._cleanups = new Set();
|
el._cleanups = new Set();
|
||||||
|
|
||||||
const boolAttrs = ["disabled", "checked", "required", "readonly", "selected", "multiple", "autofocus"];
|
const boolAttrs = ["disabled", "checked", "required", "readonly", "selected", "multiple", "autofocus"];
|
||||||
@@ -255,7 +262,13 @@ const $html = (tag, props = {}, content = []) => {
|
|||||||
el[key] = false;
|
el[key] = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
currentVal == null ? el.removeAttribute(key) : el.setAttribute(key, currentVal);
|
if (currentVal == null) {
|
||||||
|
el.removeAttribute(key);
|
||||||
|
} else if (isSVG && typeof currentVal === 'number') {
|
||||||
|
el.setAttribute(key, currentVal);
|
||||||
|
} else {
|
||||||
|
el.setAttribute(key, currentVal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
@@ -295,42 +308,41 @@ const $html = (tag, props = {}, content = []) => {
|
|||||||
return el;
|
return el;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
const $if = (condition, thenVal, otherwiseVal = null, transition = null) => {
|
||||||
* Conditional rendering component.
|
|
||||||
* @param {Function|boolean} condition - Reactive signal or boolean value.
|
|
||||||
* @param {Function|HTMLElement} thenVal - Content to show if true.
|
|
||||||
* @param {Function|HTMLElement} [otherwiseVal] - Content to show if false (optional).
|
|
||||||
* @returns {HTMLElement} A reactive container (display: contents).
|
|
||||||
*/
|
|
||||||
|
|
||||||
const $if = (condition, thenVal, otherwiseVal = null) => {
|
|
||||||
const marker = document.createTextNode("");
|
const marker = document.createTextNode("");
|
||||||
const container = $html("div", { style: "display:contents" }, [marker]);
|
const container = $html("div", { style: "display:contents" }, [marker]);
|
||||||
let current = null, last = null;
|
let current = null, last = null;
|
||||||
|
|
||||||
$watch(() => {
|
$watch(() => {
|
||||||
const state = !!(typeof condition === "function" ? condition() : condition);
|
const state = !!(typeof condition === "function" ? condition() : condition);
|
||||||
if (state !== last) {
|
if (state === last) return;
|
||||||
last = state;
|
last = state;
|
||||||
|
|
||||||
|
if (current && !state && transition?.out) {
|
||||||
|
transition.out(current.container, () => {
|
||||||
|
current.destroy();
|
||||||
|
current = null;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
if (current) current.destroy();
|
if (current) current.destroy();
|
||||||
|
current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state || (!state && otherwiseVal)) {
|
||||||
const branch = state ? thenVal : otherwiseVal;
|
const branch = state ? thenVal : otherwiseVal;
|
||||||
if (branch) {
|
if (branch) {
|
||||||
current = _view(() => typeof branch === "function" ? branch() : branch);
|
current = _view(() => typeof branch === "function" ? branch() : branch);
|
||||||
container.insertBefore(current.container, marker);
|
container.insertBefore(current.container, marker);
|
||||||
|
if (state && transition?.in) transition.in(current.container);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
};
|
};
|
||||||
|
|
||||||
$if.not = (condition, thenVal, otherwiseVal) => $if(() => !(typeof condition === "function" ? condition() : condition), thenVal, otherwiseVal);
|
$if.not = (condition, thenVal, otherwiseVal) => $if(() => !(typeof condition === "function" ? condition() : condition), thenVal, otherwiseVal);
|
||||||
|
|
||||||
/**
|
|
||||||
* Optimized reactive loop with key-based reconciliation.
|
|
||||||
* @param {Function|Array} source - Signal containing an Array of data.
|
|
||||||
* @param {Function} render - Function receiving (item, index) and returning a node.
|
|
||||||
* @param {Function} keyFn - Function to extract a unique key from the item.
|
|
||||||
* @returns {HTMLElement} A reactive container (display: contents).
|
|
||||||
*/
|
|
||||||
const $for = (source, render, keyFn, tag = "div", props = { style: "display:contents" }) => {
|
const $for = (source, render, keyFn, tag = "div", props = { style: "display:contents" }) => {
|
||||||
const marker = document.createTextNode("");
|
const marker = document.createTextNode("");
|
||||||
const container = $html(tag, props, [marker]);
|
const container = $html(tag, props, [marker]);
|
||||||
@@ -376,11 +388,6 @@ const $for = (source, render, keyFn, tag = "div", props = { style: "display:cont
|
|||||||
return container;
|
return container;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Hash-based (#) routing system.
|
|
||||||
* @param {Array<{path: string, component: Function}>} routes - Route definitions.
|
|
||||||
* @returns {HTMLElement} The router outlet container.
|
|
||||||
*/
|
|
||||||
const $router = (routes) => {
|
const $router = (routes) => {
|
||||||
const sPath = $(window.location.hash.replace(/^#/, "") || "/");
|
const sPath = $(window.location.hash.replace(/^#/, "") || "/");
|
||||||
window.addEventListener("hashchange", () => sPath(window.location.hash.replace(/^#/, "") || "/"));
|
window.addEventListener("hashchange", () => sPath(window.location.hash.replace(/^#/, "") || "/"));
|
||||||
@@ -426,21 +433,6 @@ $router.to = (path) => (window.location.hash = path.replace(/^#?\/?/, "#/"));
|
|||||||
$router.back = () => window.history.back();
|
$router.back = () => window.history.back();
|
||||||
$router.path = () => window.location.hash.replace(/^#/, "") || "/";
|
$router.path = () => window.location.hash.replace(/^#/, "") || "/";
|
||||||
|
|
||||||
/**
|
|
||||||
* Mounts a component or node into a DOM target element.
|
|
||||||
* It automatically handles the cleanup of any previously mounted SigPro instances
|
|
||||||
* in that target to prevent memory leaks and duplicate renders.
|
|
||||||
* * @param {Function|HTMLElement} component - The component function to render or a pre-built DOM node.
|
|
||||||
* @param {string|HTMLElement} target - A CSS selector string or a direct DOM element to mount into.
|
|
||||||
* @returns {Object|undefined} The view instance containing the `container` and `destroy` method, or undefined if target is not found.
|
|
||||||
* * @example
|
|
||||||
* // Mount using a component function
|
|
||||||
* $mount(() => Div({ class: "app" }, "Hello World"), "#root");
|
|
||||||
* * // Mount using a direct element
|
|
||||||
* const myApp = Div("Hello");
|
|
||||||
* $mount(myApp, document.getElementById("app"));
|
|
||||||
*/
|
|
||||||
|
|
||||||
const $mount = (component, target) => {
|
const $mount = (component, target) => {
|
||||||
const el = typeof target === "string" ? document.querySelector(target) : target;
|
const el = typeof target === "string" ? document.querySelector(target) : target;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
@@ -451,7 +443,6 @@ const $mount = (component, target) => {
|
|||||||
return instance;
|
return instance;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** GLOBAL CORE REGISTRY */
|
|
||||||
const SigProCore = { $, $watch, $html, $if, $for, $router, $mount };
|
const SigProCore = { $, $watch, $html, $if, $for, $router, $mount };
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
|
|||||||
Reference in New Issue
Block a user