Modular router && remove $$
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 9s
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 9s
This commit is contained in:
@@ -169,7 +169,7 @@ batch(() => {
|
||||
Hash‑based SPA router. Returns a DOM node that renders the current route.
|
||||
|
||||
```javascript
|
||||
import { router } from 'sigpro'
|
||||
import { router } from 'sigpro/router' // import router
|
||||
|
||||
const routes = [
|
||||
{ path: '/', component: () => div({}, 'Home') },
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
# Routing: `router( )` & Utilities
|
||||
|
||||
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.
|
||||
|
||||
## Function Signature
|
||||
|
||||
```typescript
|
||||
router(routes: Route[]): HTMLElement
|
||||
```
|
||||
|
||||
### Route Object
|
||||
|
||||
| Property | Type | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| **`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.
|
||||
|
||||
> **Availability:** `router` and its helper methods (`router.to`, `router.back`, `router.path`, `router.params`) are exported from the SigPro module. In **ESM** you must import them (`import { router } from 'sigpro'`). In the **IIFE** classic script, they are automatically available on `window`. The examples below assume the functions are already in scope.
|
||||
|
||||
---
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### 1. Defining Routes
|
||||
|
||||
Place the `router` element where you want the page content to appear. Inside the routes array, define your routes.
|
||||
|
||||
```javascript
|
||||
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: "/user/:id", component: UserProfile },
|
||||
{ path: "*", component: NotFound }
|
||||
])
|
||||
]);
|
||||
|
||||
mount(App, "#app");
|
||||
```
|
||||
|
||||
### 2. Dynamic Segments (`:id`)
|
||||
|
||||
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 hash is #/user/42
|
||||
const UserProfile = (params) => {
|
||||
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 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 `#`.
|
||||
|
||||
```javascript
|
||||
button({ onclick: () => router.to("/dashboard") }, "Go to Dashboard")
|
||||
```
|
||||
|
||||
### `router.back()`
|
||||
|
||||
Goes back one step in the browser’s history, just like calling `history.back()`.
|
||||
|
||||
```javascript
|
||||
button({ onclick: () => router.back() }, "← Back")
|
||||
```
|
||||
|
||||
### `router.path()`
|
||||
|
||||
Returns the current route path **without the leading `#`**. This is a plain string, not a signal.
|
||||
|
||||
```javascript
|
||||
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
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Styling the Router Outlet
|
||||
|
||||
The router returns a `div` with the class `"router-hook"`. You can style it just like any other element:
|
||||
|
||||
```css
|
||||
.router-hook {
|
||||
display: block;
|
||||
min-height: 60vh;
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
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 { 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. |
|
||||
@@ -1,6 +1,6 @@
|
||||
# The Signal Function: `$( )`
|
||||
# The Signal Function: `$()`
|
||||
|
||||
The `$( )` function is the core constructor of SigPro. It defines how data is stored, computed, and persisted.
|
||||
The `$()` function is the **only** reactive primitive in SigPro. It defines how data is stored, computed, and persisted. For complex nested objects, you compose signals naturally.
|
||||
|
||||
## Function Signature
|
||||
|
||||
@@ -99,175 +99,181 @@ When calling the setter, you can pass an **updater function** to access the curr
|
||||
|
||||
---
|
||||
|
||||
# The Reactive Object: `$$( )`
|
||||
## Composing Signals for Complex State
|
||||
|
||||
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
|
||||
For nested objects, **compose signals** instead of using magic proxies. This gives you explicit control over reactivity and memory.
|
||||
|
||||
### 1. Simple Object
|
||||
|
||||
<div id="demo-dollar-simple"></div>
|
||||
<div id="demo-compose-simple"></div>
|
||||
|
||||
```javascript
|
||||
{
|
||||
const state = $$({ count: 0, name: "Juan" });
|
||||
watch(() => console.log(`Count is now ${state.count}`));
|
||||
const count = $(0);
|
||||
const name = $("Juan");
|
||||
|
||||
// Optionally create a derived combined state
|
||||
const state = $(() => ({ count: count(), name: name() }));
|
||||
|
||||
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")
|
||||
p(() => `Count: ${count()}, Name: ${name()}`),
|
||||
button({ onClick: () => count(count() + 1) }, "Increment count"),
|
||||
button({ onClick: () => name(name() === "Juan" ? "Ana" : "Juan") }, "Toggle name")
|
||||
]);
|
||||
setTimeout(() => mount(App, '#demo-dollar-simple'), 50);
|
||||
setTimeout(() => mount(App, '#demo-compose-simple'), 50);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Deep Reactivity
|
||||
### 2. Deeply Nested State
|
||||
|
||||
<div id="demo-dollar-deep"></div>
|
||||
<div id="demo-compose-deep"></div>
|
||||
|
||||
```javascript
|
||||
{
|
||||
const user = $$({
|
||||
profile: {
|
||||
name: "Juan",
|
||||
address: { city: "Madrid", zip: "28001" }
|
||||
}
|
||||
});
|
||||
const profileName = $("Juan");
|
||||
const profileCity = $("Madrid");
|
||||
const profileZip = $("28001");
|
||||
|
||||
watch(() => user.profile.address.city, () => console.log("City changed"));
|
||||
// Computed derived values
|
||||
const fullAddress = $(() => `${profileCity()}, ${profileZip()}`);
|
||||
|
||||
watch(profileCity, () => console.log("City changed to:", profileCity()));
|
||||
|
||||
const App = () => div([
|
||||
p(() => `City: ${user.profile.address.city}`),
|
||||
button({ onClick: () => user.profile.address.city = "Barcelona" }, "Change to Barcelona")
|
||||
p(() => `Name: ${profileName()}`),
|
||||
p(() => `City: ${profileCity()}`),
|
||||
p(() => `Full address: ${fullAddress()}`),
|
||||
button({ onClick: () => profileCity("Barcelona") }, "Change to Barcelona")
|
||||
]);
|
||||
setTimeout(() => mount(App, '#demo-dollar-deep'), 50);
|
||||
setTimeout(() => mount(App, '#demo-compose-deep'), 50);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Arrays
|
||||
|
||||
<div id="demo-dollar-array"></div>
|
||||
<div id="demo-compose-array"></div>
|
||||
|
||||
```javascript
|
||||
{
|
||||
const todos = $$([
|
||||
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`));
|
||||
const todoCount = $(() => todos().length);
|
||||
|
||||
watch(todoCount, () => console.log(`You have ${todoCount()} todos`));
|
||||
|
||||
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")
|
||||
ul(() => todos().map(todo => li(todo.text + (todo.done ? " ✓" : "")))),
|
||||
button({ onClick: () => todos(prev => [...prev, { id: Date.now(), text: "New todo", done: false }]) }, "Add todo"),
|
||||
button({ onClick: () => {
|
||||
const updated = [...todos()];
|
||||
updated[0] = { ...updated[0], done: !updated[0].done };
|
||||
todos(updated);
|
||||
}}, "Toggle first todo")
|
||||
]);
|
||||
setTimeout(() => mount(App, '#demo-dollar-array'), 50);
|
||||
setTimeout(() => mount(App, '#demo-compose-array'), 50);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Mixed with Signals
|
||||
### 4. Complete Form Example
|
||||
|
||||
<div id="demo-dollar-mixed"></div>
|
||||
<div id="demo-compose-form"></div>
|
||||
|
||||
```javascript
|
||||
{
|
||||
const form = $$({
|
||||
fields: { email: "", password: "" },
|
||||
isValid: $(false)
|
||||
});
|
||||
const email = $("");
|
||||
const password = $("");
|
||||
const isValid = $(() => email().includes("@") && password().length > 6);
|
||||
|
||||
const canSubmit = $(() =>
|
||||
form.fields.email.includes("@") &&
|
||||
form.fields.password.length > 6
|
||||
);
|
||||
|
||||
watch(canSubmit, valid => form.isValid(valid));
|
||||
watch(isValid, valid => console.log("Form valid:", 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"}`)
|
||||
input({
|
||||
type: "email",
|
||||
placeholder: "Email",
|
||||
value: email,
|
||||
onInput: e => email(e.target.value)
|
||||
}),
|
||||
input({
|
||||
type: "password",
|
||||
placeholder: "Password",
|
||||
value: password,
|
||||
onInput: e => password(e.target.value)
|
||||
}),
|
||||
p(() => `Form valid: ${isValid() ? "Yes" : "No"}`)
|
||||
]);
|
||||
setTimeout(() => mount(App, '#demo-dollar-mixed'), 50);
|
||||
setTimeout(() => mount(App, '#demo-compose-form'), 50);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Differences: `$()` vs `$$()`
|
||||
## Best Practices for Complex State
|
||||
|
||||
| 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:
|
||||
|
||||
<div id="demo-use-dollar"></div>
|
||||
### ✅ DO: Compose signals explicitly
|
||||
|
||||
```javascript
|
||||
{
|
||||
const count = $(0);
|
||||
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);
|
||||
// Clear, predictable, and memory-safe
|
||||
const user = {
|
||||
name: $("Juan"),
|
||||
email: $("juan@example.com"),
|
||||
preferences: {
|
||||
theme: $("dark"),
|
||||
notifications: $(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Computed values derived from composition
|
||||
const userDisplay = $(() => `${user.name()} <${user.email()}>`)
|
||||
```
|
||||
|
||||
### Use `$$()` when:
|
||||
|
||||
<div id="demo-use-dollar-dollar"></div>
|
||||
### ✅ DO: Create store patterns
|
||||
|
||||
```javascript
|
||||
{
|
||||
const form = $$({ email: "", password: "" });
|
||||
const settings = $$({ theme: "dark", notifications: true });
|
||||
|
||||
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);
|
||||
const createUserStore = () => {
|
||||
const name = $("")
|
||||
const email = $("")
|
||||
|
||||
const isValid = $(() => name().length > 0 && email().includes("@"))
|
||||
|
||||
const actions = {
|
||||
setName: (value) => name(value),
|
||||
setEmail: (value) => email(value),
|
||||
reset: () => {
|
||||
name("")
|
||||
email("")
|
||||
}
|
||||
}
|
||||
|
||||
return { name, email, isValid, ...actions }
|
||||
}
|
||||
|
||||
const userStore = createUserStore()
|
||||
```
|
||||
|
||||
### ❌ DON'T: Try to wrap objects with signals
|
||||
|
||||
```javascript
|
||||
// Wrong - loses reactivity on nested properties
|
||||
const user = $({ name: "Juan", email: "..." })
|
||||
user().name = "Ana" // ❌ Not reactive!
|
||||
|
||||
// Correct - each property its own signal
|
||||
const userName = $("Juan")
|
||||
const userEmail = $("...")
|
||||
```
|
||||
|
||||
### ❌ DON'T: Destructure signals in reactive contexts
|
||||
|
||||
```javascript
|
||||
// Wrong - breaks tracking
|
||||
const { name, email } = user
|
||||
watch(() => name(), ...) // ❌ 'name' is not tracked properly
|
||||
|
||||
// Correct - use the original signal
|
||||
watch(() => user.name(), ...) // ✅
|
||||
```
|
||||
|
||||
---
|
||||
@@ -276,115 +282,121 @@ $$<T extends object>(obj: T): T
|
||||
|
||||
### ✅ DO:
|
||||
```javascript
|
||||
// Access properties directly
|
||||
state.count = 10;
|
||||
state.user.name = "Ana";
|
||||
todos.push(newItem);
|
||||
// Update by recreating objects for arrays
|
||||
todos(prev => [...prev, newTodo])
|
||||
|
||||
// Track in effects
|
||||
watch(() => state.count, () => {});
|
||||
watch(() => state.user.name, () => {});
|
||||
// Update objects immutably
|
||||
const current = user()
|
||||
user({ ...current, name: "Ana" })
|
||||
|
||||
// Track individual signals
|
||||
watch(() => user.name(), () => {})
|
||||
watch(() => user.email(), () => {})
|
||||
```
|
||||
|
||||
### ❌ DON'T:
|
||||
```javascript
|
||||
// Destructuring breaks reactivity
|
||||
const { count, user } = state; // ❌ count and user are not reactive
|
||||
// Mutate objects directly
|
||||
user().name = "Ana" // ❌ Not reactive
|
||||
|
||||
// Reassigning the whole object
|
||||
state = { count: 10 }; // ❌ Loses reactivity
|
||||
// Mutate arrays in place
|
||||
todos().push(newTodo) // ❌ Not reactive
|
||||
|
||||
// Using primitive directly
|
||||
const count = $$(0); // ❌ Doesn't work (use $() instead)
|
||||
// Destructure in component bodies
|
||||
const { name, email } = user // ❌ Breaks reactivity
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Automatic Cleanup
|
||||
|
||||
Like all SigPro reactive primitives, `$$()` integrates with the cleanup system:
|
||||
All signals integrate with the cleanup system:
|
||||
|
||||
- Effects tracking reactive properties are automatically disposed
|
||||
- No manual cleanup needed
|
||||
- Works with `watch`, `when`, and `each`
|
||||
```javascript
|
||||
// Effects are automatically disposed when components unmount
|
||||
const name = $("Juan")
|
||||
watch(name, () => console.log("Name changed"))
|
||||
|
||||
---
|
||||
|
||||
## 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) |
|
||||
// Manual cleanup if needed
|
||||
const stop = watch(name, callback)
|
||||
stop() // Clean up manually
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete Example
|
||||
|
||||
<div id="demo-complete"></div>
|
||||
<div id="demo-complete-final"></div>
|
||||
|
||||
```javascript
|
||||
{
|
||||
const app = {
|
||||
theme: $("dark", "theme_complete"),
|
||||
sidebarOpen: $(true),
|
||||
user: $$({ name: "", email: "", preferences: { notifications: true, language: "es" } }),
|
||||
isLoggedIn: $(() => !!app.user.name),
|
||||
login(name, email) {
|
||||
app.user.name = name;
|
||||
app.user.email = email;
|
||||
},
|
||||
logout() {
|
||||
app.user.name = "";
|
||||
app.user.email = "";
|
||||
app.user.preferences.notifications = true;
|
||||
}
|
||||
};
|
||||
|
||||
// All state as explicit signals
|
||||
const theme = $("dark", "theme_complete")
|
||||
const sidebarOpen = $(true)
|
||||
const userName = $("")
|
||||
const userEmail = $("")
|
||||
const notifications = $(true)
|
||||
const language = $("es")
|
||||
|
||||
// Computed signals
|
||||
const isLoggedIn = $(() => !!userName() && !!userEmail())
|
||||
|
||||
// Actions as plain functions
|
||||
const login = (name, email) => {
|
||||
userName(name)
|
||||
userEmail(email)
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
userName("")
|
||||
userEmail("")
|
||||
notifications(true) // Reset on logout
|
||||
}
|
||||
|
||||
// Components using signals directly
|
||||
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")
|
||||
]);
|
||||
input({
|
||||
placeholder: "Name",
|
||||
onInput: e => userName(e.target.value)
|
||||
}),
|
||||
input({
|
||||
placeholder: "Email",
|
||||
onInput: e => userEmail(e.target.value)
|
||||
}),
|
||||
button({
|
||||
onClick: () => login(userName(), userEmail())
|
||||
}, "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")
|
||||
]);
|
||||
h2(() => `Welcome ${userName()}`),
|
||||
p(() => `Email: ${userEmail()}`),
|
||||
p(() => `Notifications: ${notifications() ? "ON" : "OFF"}`),
|
||||
p(() => `Language: ${language()}`),
|
||||
button({
|
||||
onClick: () => notifications(!notifications())
|
||||
}, "Toggle Notifications"),
|
||||
button({ onClick: logout }, "Logout")
|
||||
])
|
||||
|
||||
const App = () => div({ class: "complete-example" }, [
|
||||
when(() => app.isLoggedIn(), () => UserProfile(), () => LoginForm())
|
||||
]);
|
||||
when(() => isLoggedIn(), () => UserProfile(), () => LoginForm())
|
||||
])
|
||||
|
||||
setTimeout(() => mount(App, '#demo-complete'), 50);
|
||||
setTimeout(() => mount(App, '#demo-complete-final'), 50)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration from `$()` to `$$()`
|
||||
## Summary
|
||||
|
||||
If you have code using nested signals:
|
||||
With **only `$()`** as your reactive primitive:
|
||||
|
||||
```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
|
||||
```
|
||||
- ✅ **Explicit** - You know exactly what's reactive
|
||||
- ✅ **Memory safe** - No hidden proxies or WeakMap caches
|
||||
- ✅ **Predictable** - No magic, just signals
|
||||
- ✅ **Performant** - Minimal overhead
|
||||
- ✅ **Debuggable** - Clear data flow
|
||||
|
||||
Complex state is built by **composing signals**, not by wrapping objects. This gives you the same power as reactive proxies but with better control and fewer surprises.
|
||||
Reference in New Issue
Block a user