Skip to content

Commit c3c5164

Browse files
committed
Add state management system with demo page and components
1 parent 923f409 commit c3c5164

File tree

7 files changed

+1363
-13
lines changed

7 files changed

+1363
-13
lines changed

public/js/README-state-manager.md

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
# State Manager for Web Components
2+
3+
A lightweight, flexible state management system designed specifically for web components and single-page applications using @profullstack/spa-router.
4+
5+
## Features
6+
7+
- **Simple API**: Easy-to-use methods for getting, setting, and subscribing to state changes
8+
- **Persistence**: Automatically saves state to localStorage (configurable)
9+
- **Targeted Subscriptions**: Subscribe to specific state keys or all state changes
10+
- **Web Component Integration**: Includes helpers for connecting web components to state
11+
- **TypeScript-friendly**: Well-defined interfaces and predictable behavior
12+
- **Router Integration**: Works seamlessly with @profullstack/spa-router
13+
14+
## Installation
15+
16+
The state manager is included directly in your project. No additional installation is required.
17+
18+
## Basic Usage
19+
20+
```javascript
21+
// Import the state manager
22+
import stateManager from './state-manager.js';
23+
24+
// Get state
25+
const counter = stateManager.getState('counter');
26+
27+
// Update state
28+
stateManager.setState({ counter: counter + 1 });
29+
30+
// Subscribe to state changes
31+
const unsubscribe = stateManager.subscribe((state, changedKeys) => {
32+
console.log('State changed:', changedKeys);
33+
console.log('New state:', state);
34+
}, 'counter');
35+
36+
// Later, unsubscribe when done
37+
unsubscribe();
38+
```
39+
40+
## API Reference
41+
42+
### StateManager Class
43+
44+
#### `constructor(initialState = {}, options = {})`
45+
46+
Creates a new state manager instance.
47+
48+
- **initialState**: Initial state object
49+
- **options**: Configuration options
50+
- **localStorageKey**: Key for localStorage (default: 'app_state')
51+
- **enablePersistence**: Whether to save state to localStorage (default: true)
52+
- **debug**: Whether to log debug messages (default: false)
53+
54+
#### `getState(key)`
55+
56+
Get the current state or a specific part of the state.
57+
58+
- **key** (optional): Specific state key to retrieve
59+
- **Returns**: The requested state
60+
61+
#### `setState(update, silent = false)`
62+
63+
Update the state.
64+
65+
- **update**: Object to merge with state or function that returns an update object
66+
- **silent** (optional): If true, don't notify subscribers
67+
- **Returns**: The new state
68+
69+
#### `subscribe(callback, keys)`
70+
71+
Subscribe to state changes.
72+
73+
- **callback**: Function to call when state changes
74+
- **keys** (optional): Specific state key(s) to subscribe to
75+
- **Returns**: Unsubscribe function
76+
77+
#### `unsubscribe(callback)`
78+
79+
Unsubscribe a callback from all subscriptions.
80+
81+
- **callback**: The callback to unsubscribe
82+
83+
#### `reset(initialState = {})`
84+
85+
Reset the state to initial values.
86+
87+
- **initialState** (optional): New initial state
88+
- **Returns**: The new state
89+
90+
### Helper Functions
91+
92+
#### `createConnectedComponent(tagName, BaseComponent, stateKeys = [], stateManager = defaultStateManager)`
93+
94+
Create a web component connected to the state manager.
95+
96+
- **tagName**: Custom element tag name
97+
- **BaseComponent**: Base component class (extends HTMLElement)
98+
- **stateKeys** (optional): State keys to subscribe to
99+
- **stateManager** (optional): State manager instance
100+
101+
#### `StateMixin(stateManager = defaultStateManager)`
102+
103+
Create a mixin for adding state management to a component.
104+
105+
- **stateManager** (optional): State manager instance
106+
- **Returns**: A mixin function
107+
108+
## Using with Web Components
109+
110+
### Option 1: Using the StateMixin
111+
112+
```javascript
113+
import { StateMixin } from './state-manager.js';
114+
115+
// Create a base component
116+
class MyComponent extends HTMLElement {
117+
constructor() {
118+
super();
119+
this.attachShadow({ mode: 'open' });
120+
}
121+
122+
connectedCallback() {
123+
this.render();
124+
}
125+
126+
render() {
127+
// Get state from the state manager
128+
const counter = this.getState('counter');
129+
130+
this.shadowRoot.innerHTML = `
131+
<div>Counter: ${counter}</div>
132+
<button id="increment">Increment</button>
133+
`;
134+
135+
this.shadowRoot.getElementById('increment').addEventListener('click', () => {
136+
this.setState({ counter: counter + 1 });
137+
});
138+
}
139+
140+
// This method is called when state changes
141+
stateChanged(state, changedKeys) {
142+
this.render();
143+
}
144+
}
145+
146+
// Apply the StateMixin to connect to the state manager
147+
const ConnectedComponent = StateMixin()(MyComponent);
148+
149+
// Register the custom element
150+
customElements.define('my-component', ConnectedComponent);
151+
```
152+
153+
### Option 2: Using createConnectedComponent
154+
155+
```javascript
156+
import { createConnectedComponent } from './state-manager.js';
157+
158+
// Create a base component
159+
class MyComponent extends HTMLElement {
160+
// Same implementation as above
161+
}
162+
163+
// Create and register the connected component
164+
createConnectedComponent('my-component', MyComponent, ['counter']);
165+
```
166+
167+
## Integration with Router
168+
169+
The state manager works seamlessly with @profullstack/spa-router:
170+
171+
```javascript
172+
// In your router configuration
173+
const routes = {
174+
'/dashboard': {
175+
view: () => loadPage('/views/dashboard.html'),
176+
beforeEnter: (to, from, next) => {
177+
// Check authentication state from state manager
178+
const user = stateManager.getState('user');
179+
if (!user || !user.loggedIn) {
180+
return next('/login');
181+
}
182+
next();
183+
}
184+
}
185+
};
186+
```
187+
188+
## Example Use Cases
189+
190+
### Authentication State
191+
192+
```javascript
193+
// Login
194+
stateManager.setState({
195+
user: {
196+
id: 123,
197+
username: 'johndoe',
198+
199+
loggedIn: true
200+
}
201+
});
202+
203+
// Logout
204+
stateManager.setState({
205+
user: {
206+
loggedIn: false
207+
}
208+
});
209+
210+
// Check auth status
211+
const isLoggedIn = stateManager.getState('user')?.loggedIn || false;
212+
```
213+
214+
### Theme Management
215+
216+
```javascript
217+
// Set theme
218+
stateManager.setState({ theme: 'dark' });
219+
220+
// Apply theme
221+
const theme = stateManager.getState('theme') || 'light';
222+
document.documentElement.setAttribute('data-theme', theme);
223+
224+
// Subscribe to theme changes
225+
stateManager.subscribe((state) => {
226+
document.documentElement.setAttribute('data-theme', state.theme || 'light');
227+
}, 'theme');
228+
```
229+
230+
### Form State
231+
232+
```javascript
233+
// Initialize form state
234+
stateManager.setState({
235+
form: {
236+
name: '',
237+
email: '',
238+
message: ''
239+
}
240+
});
241+
242+
// Update form field
243+
function updateField(field, value) {
244+
stateManager.setState({
245+
form: {
246+
...stateManager.getState('form'),
247+
[field]: value
248+
}
249+
});
250+
}
251+
252+
// Get form data
253+
const formData = stateManager.getState('form');
254+
```
255+
256+
## Best Practices
257+
258+
1. **Keep state flat**: Avoid deeply nested state objects
259+
2. **Use specific subscriptions**: Subscribe to specific keys when possible
260+
3. **Clean up subscriptions**: Always unsubscribe when components are disconnected
261+
4. **Use functions for dependent updates**:
262+
```javascript
263+
stateManager.setState(state => ({
264+
total: state.items.reduce((sum, item) => sum + item.price, 0)
265+
}));
266+
```
267+
5. **Separate UI state from application data**: Use different state keys for UI state vs. application data
268+
269+
## Demo
270+
271+
Check out the state demo page at `/state-demo` to see the state manager in action.

public/js/components/pf-header.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ class PfHeader extends HTMLElement {
287287
<a href="/dashboard" class="nav-link" id="dashboard-link">Dashboard</a>
288288
<a href="/api-docs" class="nav-link" id="api-docs-link">API Docs</a>
289289
<a href="/api-keys" class="nav-link" id="api-keys-link">API Keys</a>
290+
<a href="/state-demo" class="nav-link" id="state-demo-link">State Demo</a>
290291
<a href="/login" class="nav-link login-link" id="login-link">Login</a>
291292
<a href="/register" class="subscription-link register-link" id="register-link">Register</a>
292293
<button class="theme-toggle" title="Toggle light/dark theme">
@@ -307,6 +308,7 @@ class PfHeader extends HTMLElement {
307308
<a href="/dashboard" class="nav-link" id="mobile-dashboard-link">Dashboard</a>
308309
<a href="/api-docs" class="nav-link" id="mobile-api-docs-link">API Docs</a>
309310
<a href="/api-keys" class="nav-link" id="mobile-api-keys-link">API Keys</a>
311+
<a href="/state-demo" class="nav-link" id="mobile-state-demo-link">State Demo</a>
310312
<a href="/login" class="nav-link login-link" id="mobile-login-link">Login</a>
311313
<a href="/register" class="subscription-link register-link" id="mobile-register-link">Register</a>
312314
@@ -398,6 +400,8 @@ class PfHeader extends HTMLElement {
398400
this.shadowRoot.querySelector('#api-docs-link')?.classList.add('active');
399401
} else if (currentPath.startsWith('/api-keys')) {
400402
this.shadowRoot.querySelector('#api-keys-link')?.classList.add('active');
403+
} else if (currentPath.startsWith('/state-demo')) {
404+
this.shadowRoot.querySelector('#state-demo-link')?.classList.add('active');
401405
} else if (currentPath.startsWith('/login')) {
402406
this.shadowRoot.querySelector('#login-link')?.classList.add('active');
403407
} else if (currentPath.startsWith('/register')) {

0 commit comments

Comments
 (0)