Files
sigpro/packages/docs/api/pages.md
2026-03-20 01:11:32 +01:00

12 KiB

Pages API 📄

Pages in SigPro are special components designed for route-based navigation with automatic cleanup. When you navigate away from a page, all signals, effects, and event listeners created within that page are automatically cleaned up - no memory leaks, no manual cleanup needed.

$.page(setupFunction)

Creates a page with automatic cleanup of all signals and effects when navigated away.

import { $, html } from 'sigpro';

export default $.page(() => {
  // All signals and effects created here
  // will be automatically cleaned up on navigation
  const count = $(0);
  
  $.effect(() => {
    console.log(`Count: ${count()}`);
  });
  
  return html`
    <div>
      <h1>My Page</h1>
      <p>Count: ${count}</p>
      <button @click=${() => count(c => c + 1)}>+</button>
    </div>
  `;
});

📋 API Reference

Parameter Type Description
setupFunction Function Function that returns the page content. Receives context object with params and onUnmount

Context Object Properties

Property Type Description
params Object Route parameters passed to the page
onUnmount Function Register cleanup callbacks (alternative to automatic cleanup)

🎯 Basic Usage

Simple Page

// pages/home.js
import { $, html } from 'sigpro';

export default $.page(() => {
  const title = $('Welcome to SigPro');
  
  return html`
    <div class="home-page">
      <h1>${title}</h1>
      <p>This page will clean itself up when you navigate away.</p>
    </div>
  `;
});

Page with Route Parameters

// pages/user.js
import { $, html } from 'sigpro';

export default $.page(({ params }) => {
  // Access route parameters
  const userId = params.id;
  const userData = $(null);
  const loading = $(false);
  
  // Auto-cleaned effect
  $.effect(() => {
    loading(true);
    $.fetch(`/api/users/${userId}`, null, loading)
      .then(data => userData(data));
  });
  
  return html`
    <div>
      ${() => loading() ? html`
        <div class="spinner">Loading...</div>
      ` : html`
        <h1>User Profile: ${userData()?.name}</h1>
        <p>Email: ${userData()?.email}</p>
      `}
    </div>
  `;
});

🧹 Automatic Cleanup

The magic of $.page is automatic cleanup. Everything created inside the page is tracked and cleaned up:

export default $.page(() => {
  // ✅ Signals are auto-cleaned
  const count = $(0);
  const user = $(null);
  
  // ✅ Effects are auto-cleaned
  $.effect(() => {
    document.title = `Count: ${count()}`;
  });
  
  // ✅ Event listeners are auto-cleaned
  window.addEventListener('resize', handleResize);
  
  // ✅ Intervals and timeouts are auto-cleaned
  const interval = setInterval(() => {
    refreshData();
  }, 5000);
  
  return html`<div>Page content</div>`;
});
// When navigating away: all signals, effects, listeners, intervals STOP

📝 Manual Cleanup with onUnmount

Sometimes you need custom cleanup logic. Use onUnmount for that:

export default $.page(({ onUnmount }) => {
  // WebSocket connection
  const socket = new WebSocket('wss://api.example.com');
  
  socket.onmessage = (event) => {
    updateData(JSON.parse(event.data));
  };
  
  // Manual cleanup
  onUnmount(() => {
    socket.close();
    console.log('WebSocket closed');
  });
  
  return html`<div>Real-time updates</div>`;
});

🔄 Integration with Router

Pages are designed to work seamlessly with $.router:

import { $, html } from 'sigpro';
import HomePage from './pages/Home.js';
import UserPage from './pages/User.js';
import SettingsPage from './pages/Settings.js';

const routes = [
  { path: '/', component: HomePage },
  { path: '/user/:id', component: UserPage },
  { path: '/settings', component: SettingsPage },
];

// Mount router
document.body.appendChild($.router(routes));

💡 Practical Examples

Example 1: Data Fetching Page

// pages/posts.js
export default $.page(({ params }) => {
  const posts = $([]);
  const loading = $(true);
  const error = $(null);
  
  $.effect(() => {
    fetch('/api/posts')
      .then(res => res.json())
      .then(data => {
        posts(data);
        loading(false);
      })
      .catch(err => {
        error(err.message);
        loading(false);
      });
  });
  
  return html`
    <div class="posts-page">
      <h1>Blog Posts</h1>
      
      ${() => loading() ? html`
        <div class="loading">Loading posts...</div>
      ` : error() ? html`
        <div class="error">Error: ${error()}</div>
      ` : html`
        <div class="posts-grid">
          ${posts().map(post => html`
            <article class="post-card">
              <h2>${post.title}</h2>
              <p>${post.excerpt}</p>
              <a href="#/post/${post.id}">Read more</a>
            </article>
          `)}
        </div>
      `}
    </div>
  `;
});

Example 2: Real-time Dashboard

// pages/dashboard.js
export default $.page(({ onUnmount }) => {
  const metrics = $({
    cpu: 0,
    memory: 0,
    requests: 0
  });
  
  // Auto-refresh data
  const refreshInterval = setInterval(async () => {
    const data = await $.fetch('/api/metrics');
    if (data) metrics(data);
  }, 5000);
  
  // Manual cleanup for interval
  onUnmount(() => clearInterval(refreshInterval));
  
  // Live clock
  const currentTime = $(new Date());
  const clockInterval = setInterval(() => {
    currentTime(new Date());
  }, 1000);
  
  onUnmount(() => clearInterval(clockInterval));
  
  return html`
    <div class="dashboard">
      <h1>System Dashboard</h1>
      
      <div class="time">
        Last updated: ${() => currentTime().toLocaleTimeString()}
      </div>
      
      <div class="metrics-grid">
        <div class="metric-card">
          <h3>CPU Usage</h3>
          <p class="metric-value">${() => metrics().cpu}%</p>
        </div>
        <div class="metric-card">
          <h3>Memory Usage</h3>
          <p class="metric-value">${() => metrics().memory}%</p>
        </div>
        <div class="metric-card">
          <h3>Requests/min</h3>
          <p class="metric-value">${() => metrics().requests}</p>
        </div>
      </div>
    </div>
  `;
});

Example 3: Multi-step Form

// pages/checkout.js
export default $.page(({ onUnmount }) => {
  const step = $(1);
  const formData = $({
    email: '',
    address: '',
    payment: ''
  });
  
  // Warn user before leaving
  const handleBeforeUnload = (e) => {
    if (step() < 3) {
      e.preventDefault();
      e.returnValue = '';
    }
  };
  
  window.addEventListener('beforeunload', handleBeforeUnload);
  onUnmount(() => {
    window.removeEventListener('beforeunload', handleBeforeUnload);
  });
  
  const nextStep = () => step(s => Math.min(s + 1, 3));
  const prevStep = () => step(s => Math.max(s - 1, 1));
  
  return html`
    <div class="checkout">
      <h1>Checkout - Step ${step} of 3</h1>
      
      ${() => {
        switch(step()) {
          case 1:
            return html`
              <div class="step">
                <h2>Email</h2>
                <input 
                  type="email" 
                  :value=${() => formData().email}
                  @input=${(e) => formData({...formData(), email: e.target.value})}
                />
              </div>
            `;
          case 2:
            return html`
              <div class="step">
                <h2>Address</h2>
                <textarea 
                  :value=${() => formData().address}
                  @input=${(e) => formData({...formData(), address: e.target.value})}
                ></textarea>
              </div>
            `;
          case 3:
            return html`
              <div class="step">
                <h2>Payment</h2>
                <input 
                  type="text" 
                  placeholder="Card number"
                  :value=${() => formData().payment}
                  @input=${(e) => formData({...formData(), payment: e.target.value})}
                />
              </div>
            `;
        }
      }}
      
      <div class="buttons">
        ${() => step() > 1 ? html`
          <button @click=${prevStep}>Previous</button>
        ` : ''}
        
        ${() => step() < 3 ? html`
          <button @click=${nextStep}>Next</button>
        ` : html`
          <button @click=${submitOrder}>Place Order</button>
        `}
      </div>
    </div>
  `;
});

Example 4: Page with Tabs

// pages/profile.js
export default $.page(({ params }) => {
  const activeTab = $('overview');
  const userData = $(null);
  
  // Load user data
  $.effect(() => {
    $.fetch(`/api/users/${params.id}`)
      .then(data => userData(data));
  });
  
  const tabs = {
    overview: () => html`
      <div>
        <h3>Overview</h3>
        <p>Username: ${userData()?.username}</p>
        <p>Member since: ${userData()?.joined}</p>
      </div>
    `,
    posts: () => html`
      <div>
        <h3>Posts</h3>
        ${userData()?.posts.map(post => html`
          <div class="post">${post.title}</div>
        `)}
      </div>
    `,
    settings: () => html`
      <div>
        <h3>Settings</h3>
        <label>
          <input type="checkbox" :checked=${userData()?.emailNotifications} />
          Email notifications
        </label>
      </div>
    `
  };
  
  return html`
    <div class="profile-page">
      <h1>${() => userData()?.name}</h1>
      
      <div class="tabs">
        ${Object.keys(tabs).map(tab => html`
          <button 
            class:active=${() => activeTab() === tab}
            @click=${() => activeTab(tab)}
          >
            ${tab.charAt(0).toUpperCase() + tab.slice(1)}
          </button>
        `)}
      </div>
      
      <div class="tab-content">
        ${() => tabs[activeTab()]()}
      </div>
    </div>
  `;
});

🎯 Advanced Patterns

Page with Nested Routes

// pages/settings/index.js
export default $.page(({ params }) => {
  const section = params.section || 'general';
  
  const sections = {
    general: () => import('./general.js').then(m => m.default),
    security: () => import('./security.js').then(m => m.default),
    notifications: () => import('./notifications.js').then(m => m.default)
  };
  
  const currentSection = $(null);
  
  $.effect(() => {
    sections[section]().then(comp => currentSection(comp));
  });
  
  return html`
    <div class="settings">
      <nav>
        <a href="#/settings/general">General</a>
        <a href="#/settings/security">Security</a>
        <a href="#/settings/notifications">Notifications</a>
      </nav>
      
      <div class="content">
        ${currentSection}
      </div>
    </div>
  `;
});

Page with Authentication

// pages/dashboard.js
export default $.page(({ onUnmount }) => {
  const isAuthenticated = $(false);
  const authCheck = $.effect(() => {
    const token = localStorage.getItem('token');
    isAuthenticated(!!token);
  });
  
  // Redirect if not authenticated
  $.effect(() => {
    if (!isAuthenticated()) {
      $.router.go('/login');
    }
  });
  
  return html`
    <div class="dashboard">
      <h1>Protected Dashboard</h1>
      <!-- Protected content -->
    </div>
  `;
});

📊 Summary

Feature Description
Automatic Cleanup All signals, effects, and resources auto-cleaned on navigation
Memory Safe No memory leaks, even with complex nested effects
Router Integration Designed to work perfectly with $.router
Parameters Access route parameters via params object
Manual Cleanup onUnmount for custom cleanup needs
Zero Configuration Just wrap your page in $.page() and it works

Pro Tip: Always wrap route-based views in $.page() to ensure proper cleanup. This prevents memory leaks and ensures your app stays performant even after many navigation changes.