watch-selector
Version:
Runs a function when a selector is added to dom
1,646 lines (1,598 loc) • 60.9 kB
text/typescript
/**
* State Management with Sync Generators
*
* This module provides state management functions that work with sync generators
* and the yield* pattern for better type safety and consistency.
*/
import type { Workflow, WatchContext, Operation } from "../types";
// ============================================================================
// State Management Functions
// ============================================================================
/**
* Set state value for the current element.
*
* Stores a value in the element's state map with type safety. Each element
* maintains its own isolated state that persists across DOM mutations.
*
* @template T - Type of the value being stored
* @param key - State key identifier (string)
* @param value - Value to store in state
* @returns Workflow<void> - Generator workflow for yield*
*
* @example Basic state setting
* ```typescript
* watch('.counter', function* () {
* yield* setState('count', 0);
* yield* setState('user', { name: 'John', age: 30 });
* });
* ```
*
* @example With type safety
* ```typescript
* interface UserData {
* name: string;
* age: number;
* email: string;
* }
*
* watch('.profile', function* () {
* const userData: UserData = {
* name: 'Jane',
* age: 25,
* email: 'jane@example.com'
* };
* yield* setState<UserData>('user', userData);
* });
* ```
*
* @example Setting multiple states
* ```typescript
* watch('.form', function* () {
* yield* setState('isValid', false);
* yield* setState('errors', []);
* yield* setState('submitCount', 0);
* yield* setState('lastSubmit', new Date());
* });
* ```
*/
export function setState<T>(key: string, value: T): Workflow<void> {
return (function* (): Generator<Operation<void>, void, any> {
yield ((context: WatchContext) => {
if (!context.state) {
(context as any).state = new Map();
}
context.state.set(key, value);
}) as Operation<void>;
})();
}
/**
* Explicit generator version of setState for guaranteed workflow behavior.
*
* This .gen version always returns a Workflow and provides explicit control
* over generator behavior for complex state management scenarios.
*
* @template T - Type of the value being stored
* @param key - State key identifier
* @param value - Value to store in state
* @returns Workflow<void> - Always returns a workflow for yield*
*
* @example Explicit generator usage
* ```typescript
* watch('.component', function* () {
* // Explicit .gen version - guaranteed workflow
* yield* setState.gen('config', { theme: 'dark', lang: 'en' });
* yield* setState.gen('initialized', true);
* });
* ```
*
* @example Complex state management
* ```typescript
* watch('.data-table', function* () {
* yield* setState.gen('loading', true);
* yield* setState.gen('data', []);
* yield* setState.gen('error', null);
*
* try {
* const data = yield* fetchData();
* yield* setState.gen('data', data);
* yield* setState.gen('loading', false);
* } catch (error) {
* yield* setState.gen('error', error.message);
* yield* setState.gen('loading', false);
* }
* });
* ```
*/
setState.gen = function <T>(key: string, value: T): Workflow<void> {
return setState<T>(key, value);
};
/**
* Get state value for the current element with optional default value.
*
* Retrieves a value from the element's state map with type safety.
* Returns the default value if the key doesn't exist or state is uninitialized.
*
* @template T - Expected type of the stored value
* @param key - State key identifier to retrieve
* @param defaultValue - Optional default value if key doesn't exist
* @returns Workflow<T | undefined> - The stored value, default value, or undefined
*
* @example Basic state retrieval
* ```typescript
* watch('.counter', function* () {
* const count = yield* getState<number>('count', 0);
* yield* text(`Count: ${count}`);
* });
* ```
*
* @example With complex types
* ```typescript
* interface Settings {
* theme: 'light' | 'dark';
* notifications: boolean;
* language: string;
* }
*
* watch('.settings-panel', function* () {
* const settings = yield* getState<Settings>('userSettings', {
* theme: 'light',
* notifications: true,
* language: 'en'
* });
*
* yield* toggleClass('dark-theme', settings.theme === 'dark');
* });
* ```
*
* @example Conditional logic based on state
* ```typescript
* watch('.widget', function* () {
* const isInitialized = yield* getState<boolean>('initialized');
*
* if (!isInitialized) {
* yield* addClass('loading');
* yield* initializeWidget();
* yield* setState('initialized', true);
* yield* removeClass('loading');
* }
* });
* ```
*/
export function getState<T>(
key: string,
defaultValue?: T,
): Workflow<T | undefined> {
return (function* (): Generator<
Operation<T | undefined>,
T | undefined,
any
> {
const result = yield ((context: WatchContext) => {
if (!context.state) {
return defaultValue;
}
return context.state.has(key)
? (context.state.get(key) as T)
: defaultValue;
}) as Operation<T | undefined>;
return result;
})();
}
/**
* Explicit generator version of getState for guaranteed workflow behavior.
*
* This .gen version always returns a Workflow and provides explicit control
* over generator behavior for complex state retrieval scenarios.
*
* @template T - Expected type of the stored value
* @param key - State key identifier to retrieve
* @param defaultValue - Optional default value if key doesn't exist
* @returns Workflow<T | undefined> - Always returns a workflow for yield*
*
* @example Explicit generator usage
* ```typescript
* watch('.component', function* () {
* // Explicit .gen version - guaranteed workflow
* const config = yield* getState.gen<Config>('config', defaultConfig);
* const isReady = yield* getState.gen<boolean>('ready', false);
*
* if (isReady && config.autoStart) {
* yield* startComponent();
* }
* });
* ```
*
* @example Complex state retrieval with fallbacks
* ```typescript
* watch('.user-profile', function* () {
* const user = yield* getState.gen<User>('user');
* const preferences = yield* getState.gen<Preferences>('preferences', defaultPrefs);
*
* if (user) {
* yield* text('.username', user.name);
* yield* attr('.avatar', 'src', user.avatar);
* } else {
* yield* addClass('anonymous');
* }
* });
* ```
*/
getState.gen = function <T>(
key: string,
defaultValue?: T,
): Workflow<T | undefined> {
return getState<T>(key, defaultValue);
};
/**
* Update state value using an updater function with atomic operation.
*
* Safely updates state by passing the current value to an updater function
* and storing the result. This ensures consistent state updates even with
* complex transformations.
*
* @template T - Type of the state value being updated
* @param key - State key identifier to update
* @param updater - Function that receives current value and returns new value
* @returns Workflow<void> - Generator workflow for yield*
*
* @example Counter increment
* ```typescript
* watch('.counter', function* () {
* yield* click(function* () {
* yield* updateState<number>('count', (current = 0) => current + 1);
* const newCount = yield* getState<number>('count', 0);
* yield* text(`Count: ${newCount}`);
* });
* });
* ```
*
* @example Array manipulation
* ```typescript
* watch('.todo-list', function* () {
* yield* click('.add-item', function* () {
* const newItem = yield* value<string>('.new-item-input');
*
* yield* updateState<string[]>('items', (items = []) => [
* ...items,
* newItem
* ]);
*
* yield* value('.new-item-input', ''); // Clear input
* });
* });
* ```
*
* @example Object updates with spread
* ```typescript
* watch('.user-form', function* () {
* yield* input('.name-field', function* (e) {
* const name = e.target.value;
*
* yield* updateState<User>('user', (user = {}) => ({
* ...user,
* name,
* lastUpdated: new Date()
* }));
* });
* });
* ```
*
* @example Complex state transformation
* ```typescript
* watch('.game-board', function* () {
* yield* click('.cell', function* (e) {
* const cellIndex = parseInt(e.target.dataset.index || '0');
*
* yield* updateState<GameState>('gameState', (state = defaultGameState) => {
* if (state.board[cellIndex] !== null || state.gameOver) {
* return state; // Invalid move
* }
*
* const newBoard = [...state.board];
* newBoard[cellIndex] = state.currentPlayer;
*
* return {
* ...state,
* board: newBoard,
* currentPlayer: state.currentPlayer === 'X' ? 'O' : 'X',
* moveCount: state.moveCount + 1
* };
* });
* });
* });
* ```
*/
export function updateState<T>(
key: string,
updater: (value: T | undefined) => T,
): Workflow<void> {
return (function* (): Generator<Operation<void>, void, any> {
yield ((context: WatchContext) => {
if (!context.state) {
(context as any).state = new Map();
}
const currentValue = context.state.get(key) as T | undefined;
const newValue = updater(currentValue);
context.state.set(key, newValue);
}) as Operation<void>;
})();
}
/**
* Explicit generator version of updateState for guaranteed workflow behavior.
*
* This .gen version always returns a Workflow and provides explicit control
* over generator behavior for complex atomic state updates.
*
* @template T - Type of the state value being updated
* @param key - State key identifier to update
* @param updater - Function that receives current value and returns new value
* @returns Workflow<void> - Always returns a workflow for yield*
*
* @example Explicit generator usage
* ```typescript
* watch('.shopping-cart', function* () {
* yield* click('.add-to-cart', function* () {
* // Explicit .gen version for guaranteed workflow
* yield* updateState.gen<CartItem[]>('items', (items = []) => {
* const productId = yield* attr('data-product-id');
* const existingItem = items.find(item => item.id === productId);
*
* if (existingItem) {
* return items.map(item =>
* item.id === productId
* ? { ...item, quantity: item.quantity + 1 }
* : item
* );
* } else {
* return [...items, { id: productId, quantity: 1 }];
* }
* });
* });
* });
* ```
*/
updateState.gen = function <T>(
key: string,
updater: (value: T | undefined) => T,
): Workflow<void> {
return updateState<T>(key, updater);
};
/**
* Check if a state key exists for the current element.
*
* Returns true if the specified key exists in the element's state map,
* false otherwise. Useful for conditional logic based on state presence.
*
* @param key - State key identifier to check
* @returns Workflow<boolean> - true if key exists, false otherwise
*
* @example Conditional initialization
* ```typescript
* watch('.widget', function* () {
* const isInitialized = yield* hasState('initialized');
*
* if (!isInitialized) {
* yield* setState('config', defaultConfig);
* yield* setState('initialized', true);
* yield* addClass('ready');
* }
* });
* ```
*
* @example State validation
* ```typescript
* watch('.form', function* () {
* const hasErrors = yield* hasState('validationErrors');
* const hasData = yield* hasState('formData');
*
* yield* toggleClass('has-errors', hasErrors);
* yield* toggleClass('has-data', hasData);
*
* if (hasData && !hasErrors) {
* yield* removeClass('submit-btn', 'disabled');
* }
* });
* ```
*
* @example Cache management
* ```typescript
* watch('.data-display', function* () {
* const hasCachedData = yield* hasState('cachedResults');
*
* if (hasCachedData) {
* const data = yield* getState('cachedResults');
* yield* renderData(data);
* } else {
* yield* addClass('loading');
* const freshData = yield* fetchData();
* yield* setState('cachedResults', freshData);
* yield* renderData(freshData);
* yield* removeClass('loading');
* }
* });
* ```
*/
export function hasState(key: string): Workflow<boolean> {
return (function* (): Generator<Operation<boolean>, boolean, any> {
const result = yield ((context: WatchContext) => {
if (!context.state) {
return false;
}
return context.state.has(key);
}) as Operation<boolean>;
return result;
})();
}
/**
* Explicit generator version of hasState for guaranteed workflow behavior.
*
* This .gen version always returns a Workflow and provides explicit control
* over generator behavior for state existence checks.
*
* @param key - State key identifier to check
* @returns Workflow<boolean> - Always returns a workflow for yield*
*
* @example Explicit generator usage
* ```typescript
* watch('.component', function* () {
* // Explicit .gen version - guaranteed workflow
* const hasConfig = yield* hasState.gen('configuration');
* const hasUser = yield* hasState.gen('currentUser');
*
* if (hasConfig && hasUser) {
* yield* addClass('fully-initialized');
* }
* });
* ```
*/
hasState.gen = function (key: string): Workflow<boolean> {
return hasState(key);
};
/**
* Delete a state key from the current element's state.
*
* Removes the specified key and its value from the element's state map.
* Returns true if the key existed and was deleted, false otherwise.
*
* @param key - State key identifier to delete
* @returns Workflow<boolean> - true if key was deleted, false if it didn't exist
*
* @example Cleanup temporary state
* ```typescript
* watch('.modal', function* () {
* yield* click('.close', function* () {
* yield* deleteState('temporaryData');
* yield* deleteState('userInput');
* yield* deleteState('validationErrors');
* yield* addClass('closed');
* });
* });
* ```
*
* @example Conditional cleanup
* ```typescript
* watch('.cache-manager', function* () {
* yield* click('.clear-expired', function* () {
* const expiredKeys = yield* getState<string[]>('expiredKeys', []);
*
* for (const key of expiredKeys) {
* const wasDeleted = yield* deleteState(key);
* if (wasDeleted) {
* console.log(`Cleared expired cache: ${key}`);
* }
* }
*
* yield* deleteState('expiredKeys');
* });
* });
* ```
*
* @example State lifecycle management
* ```typescript
* watch('.session-manager', function* () {
* yield* onUnmount(function* () {
* // Clean up sensitive data on component unmount
* yield* deleteState('authToken');
* yield* deleteState('userData');
* yield* deleteState('sessionId');
* });
* });
* ```
*/
export function deleteState(key: string): Workflow<boolean> {
return (function* (): Generator<Operation<boolean>, boolean, any> {
const result = yield ((context: WatchContext) => {
if (!context.state) {
return false;
}
return context.state.delete(key);
}) as Operation<boolean>;
return result;
})();
}
/**
* Explicit generator version of deleteState for guaranteed workflow behavior.
*
* This .gen version always returns a Workflow and provides explicit control
* over generator behavior for state key deletion.
*
* @param key - State key identifier to delete
* @returns Workflow<boolean> - Always returns a workflow for yield*
*
* @example Explicit generator usage
* ```typescript
* watch('.cleanup-manager', function* () {
* // Explicit .gen version - guaranteed workflow
* const deleted = yield* deleteState.gen('temporaryConfig');
*
* if (deleted) {
* yield* text('.status', 'Temporary config cleared');
* }
* });
* ```
*/
deleteState.gen = function (key: string): Workflow<boolean> {
return deleteState(key);
};
/**
* Clear all state for the current element.
*
* Removes all key-value pairs from the element's state map, effectively
* resetting the element to have no stored state.
*
* @returns Workflow<void> - Generator workflow for yield*
*
* @example Reset component state
* ```typescript
* watch('.game-board', function* () {
* yield* click('.reset-game', function* () {
* yield* clearState();
* yield* removeClass('game-over');
* yield* addClass('ready-to-play');
* yield* text('.status', 'Game reset - click to start!');
* });
* });
* ```
*
* @example Form reset with state cleanup
* ```typescript
* watch('.contact-form', function* () {
* yield* click('.reset-form', function* () {
* // Clear all form state
* yield* clearState();
*
* // Reset form UI
* yield* removeClass('has-errors');
* yield* removeClass('submitted');
* yield* text('.error-messages', '');
*
* // Reset form fields
* yield* value('input', '');
* yield* value('textarea', '');
* });
* });
* ```
*
* @example Session cleanup
* ```typescript
* watch('.user-session', function* () {
* yield* click('.logout', function* () {
* // Clear all session state
* yield* clearState();
*
* // Update UI to logged-out state
* yield* addClass('logged-out');
* yield* removeClass('authenticated');
* yield* text('.username', 'Guest');
* });
* });
* ```
*/
export function clearState(): Workflow<void> {
return (function* (): Generator<Operation<void>, void, any> {
yield ((context: WatchContext) => {
if (context.state) {
context.state.clear();
}
}) as Operation<void>;
})();
}
/**
* Explicit generator version of clearState for guaranteed workflow behavior.
*
* This .gen version always returns a Workflow and provides explicit control
* over generator behavior for complete state clearing.
*
* @returns Workflow<void> - Always returns a workflow for yield*
*
* @example Explicit generator usage
* ```typescript
* watch('.admin-panel', function* () {
* yield* click('.emergency-reset', function* () {
* // Explicit .gen version - guaranteed workflow
* yield* clearState.gen();
* yield* addClass('emergency-mode');
* yield* text('.status', 'All state cleared - emergency reset complete');
* });
* });
* ```
*/
clearState.gen = function (): Workflow<void> {
return clearState();
};
/**
* Get all state keys for the current element.
*
* Returns an array of all state key names stored in the element's state map.
* Useful for debugging, state inspection, or bulk state operations.
*
* @returns Workflow<string[]> - Array of all state key names
*
* @example State debugging
* ```typescript
* watch('.debug-panel', function* () {
* yield* click('.show-state', function* () {
* const keys = yield* getStateKeys();
* yield* text('.state-keys', `State keys: ${keys.join(', ')}`);
*
* if (keys.length === 0) {
* yield* addClass('no-state');
* yield* text('.state-info', 'No state data found');
* }
* });
* });
* ```
*
* @example State validation
* ```typescript
* watch('.form-validator', function* () {
* yield* click('.validate', function* () {
* const keys = yield* getStateKeys();
* const requiredKeys = ['username', 'email', 'password'];
*
* const missingKeys = requiredKeys.filter(key => !keys.includes(key));
*
* if (missingKeys.length > 0) {
* yield* addClass('incomplete');
* yield* text('.errors', `Missing: ${missingKeys.join(', ')}`);
* } else {
* yield* removeClass('incomplete');
* yield* addClass('valid');
* }
* });
* });
* ```
*
* @example State export functionality
* ```typescript
* watch('.data-export', function* () {
* yield* click('.export-state', function* () {
* const keys = yield* getStateKeys();
* const stateData: Record<string, any> = {};
*
* for (const key of keys) {
* stateData[key] = yield* getState(key);
* }
*
* yield* attr('.export-link', 'href',
* `data:application/json,${encodeURIComponent(JSON.stringify(stateData))}`
* );
* yield* removeClass('.export-link', 'hidden');
* });
* });
* ```
*/
export function getStateKeys(): Workflow<string[]> {
return (function* (): Generator<Operation<string[]>, string[], any> {
const result = yield ((context: WatchContext) => {
if (!context.state) {
return [];
}
return Array.from(context.state.keys());
}) as Operation<string[]>;
return result;
})();
}
/**
* Explicit generator version of getStateKeys for guaranteed workflow behavior.
*
* This .gen version always returns a Workflow and provides explicit control
* over generator behavior for state key retrieval.
*
* @returns Workflow<string[]> - Always returns a workflow for yield*
*
* @example Explicit generator usage
* ```typescript
* watch('.state-inspector', function* () {
* // Explicit .gen version - guaranteed workflow
* const keys = yield* getStateKeys.gen();
*
* yield* text('.key-count', `${keys.length} state keys found`);
* yield* html('.key-list', keys.map(key => `<li>${key}</li>`).join(''));
* });
* ```
*/
getStateKeys.gen = function (): Workflow<string[]> {
return getStateKeys();
};
/**
* Get all state entries as key-value pairs for the current element.
*
* Returns an array of [key, value] tuples representing all state data
* stored in the element's state map. Useful for state inspection,
* serialization, or bulk operations.
*
* @template T - Expected type of state values (defaults to any)
* @returns Workflow<Array<[string, T]>> - Array of [key, value] tuples
*
* @example State inspection
* ```typescript
* watch('.state-viewer', function* () {
* yield* click('.show-all-state', function* () {
* const entries = yield* getStateEntries<any>();
*
* if (entries.length === 0) {
* yield* text('.state-display', 'No state data found');
* } else {
* const stateHtml = entries
* .map(([key, value]) => `<div><strong>${key}:</strong> ${JSON.stringify(value)}</div>`)
* .join('');
* yield* html('.state-display', stateHtml);
* }
* });
* });
* ```
*
* @example State serialization
* ```typescript
* watch('.export-manager', function* () {
* yield* click('.export-data', function* () {
* const entries = yield* getStateEntries<any>();
* const stateObject = Object.fromEntries(entries);
*
* yield* attr('.download-link', 'href',
* `data:application/json,${encodeURIComponent(JSON.stringify(stateObject))}`
* );
* yield* attr('.download-link', 'download', 'state-export.json');
* });
* });
* ```
*
* @example State comparison
* ```typescript
* watch('.state-diff', function* () {
* yield* click('.compare-state', function* () {
* const currentEntries = yield* getStateEntries<any>();
* const previousEntries = yield* getState<Array<[string, any]>>('previousState', []);
*
* const changes = currentEntries.filter(([key, value]) => {
* const prev = previousEntries.find(([prevKey]) => prevKey === key);
* return !prev || prev[1] !== value;
* });
*
* yield* setState('previousState', currentEntries);
* yield* text('.changes-count', `${changes.length} changes detected`);
* });
* });
* ```
*/
export function getStateEntries<T = any>(): Workflow<Array<[string, T]>> {
return (function* (): Generator<
Operation<Array<[string, T]>>,
Array<[string, T]>,
any
> {
const result = yield ((context: WatchContext) => {
if (!context.state) {
return [];
}
return Array.from(context.state.entries()) as Array<[string, T]>;
}) as Operation<Array<[string, T]>>;
return result;
})();
}
/**
* Explicit generator version of getStateEntries for guaranteed workflow behavior.
*
* This .gen version always returns a Workflow and provides explicit control
* over generator behavior for state entries retrieval.
*
* @template T - Expected type of state values
* @returns Workflow<Array<[string, T]>> - Always returns a workflow for yield*
*
* @example Explicit generator usage
* ```typescript
* watch('.state-manager', function* () {
* // Explicit .gen version - guaranteed workflow
* const entries = yield* getStateEntries.gen<any>();
*
* for (const [key, value] of entries) {
* yield* text(`.state-${key}`, String(value));
* }
* });
* ```
*/
getStateEntries.gen = function <T = any>(): Workflow<Array<[string, T]>> {
return getStateEntries<T>();
};
/**
* Get the number of state entries for the current element.
*
* Returns the count of key-value pairs stored in the element's state map.
* Useful for state monitoring, debugging, and conditional logic.
*
* @returns Workflow<number> - Number of state entries
*
* @example State monitoring
* ```typescript
* watch('.state-monitor', function* () {
* yield* input('.data-input', function* () {
* const size = yield* getStateSize();
* yield* text('.state-count', `${size} items in state`);
*
* if (size > 10) {
* yield* addClass('state-warning');
* yield* text('.warning', 'Large state detected - consider cleanup');
* }
* });
* });
* ```
*
* @example Conditional cleanup
* ```typescript
* watch('.memory-manager', function* () {
* yield* setInterval(function* () {
* const size = yield* getStateSize();
*
* if (size > 50) {
* // Trigger cleanup for large state
* const keys = yield* getStateKeys();
* const tempKeys = keys.filter(key => key.startsWith('temp_'));
*
* for (const key of tempKeys) {
* yield* deleteState(key);
* }
* }
* }, 30000); // Check every 30 seconds
* });
* ```
*
* @example State validation
* ```typescript
* watch('.form-validator', function* () {
* yield* click('.validate-completeness', function* () {
* const size = yield* getStateSize();
* const requiredFieldCount = 5;
*
* if (size < requiredFieldCount) {
* yield* addClass('incomplete-form');
* yield* text('.validation-error',
* `Missing ${requiredFieldCount - size} required fields`
* );
* } else {
* yield* removeClass('incomplete-form');
* yield* addClass('form-complete');
* }
* });
* });
* ```
*/
export function getStateSize(): Workflow<number> {
return (function* (): Generator<Operation<number>, number, any> {
const result = yield ((context: WatchContext) => {
if (!context.state) {
return 0;
}
return context.state.size;
}) as Operation<number>;
return result;
})();
}
/**
* Explicit generator version of persistState for guaranteed workflow behavior.
*
* This .gen version always returns a Workflow and provides explicit control
* over generator behavior for state persistence.
*
* @param key - State key to persist
* @param storageKey - Optional custom localStorage key
* @returns Workflow<void> - Always returns a workflow for yield*
*
* @example Explicit generator usage
* ```typescript
* watch('.auto-saver', function* () {
* yield* setInterval(function* () {
* // Explicit .gen version - guaranteed workflow
* yield* persistState.gen('documentContent', 'auto_save_draft');
* yield* persistState.gen('lastSaved', 'auto_save_timestamp');
*
* yield* text('.save-indicator', 'Auto-saved');
* }, 30000);
* });
* ```
*/
persistState.gen = function (key: string, storageKey?: string): Workflow<void> {
return persistState(key, storageKey);
};
/**
* Explicit generator version of computedState for guaranteed workflow behavior.
*
* This .gen version always returns a Workflow and provides explicit control
* over generator behavior for computed state values.
*
* @template T - Type of the computed value
* @param key - Key where the computed value will be cached
* @param dependencies - Array of state keys that this computation depends on
* @param compute - Function that computes the value from dependency values
* @returns Workflow<T> - Always returns a workflow for yield*
*
* @example Explicit generator usage
* ```typescript
* watch('.computed-dashboard', function* () {
* // Explicit .gen version - guaranteed workflow
* const summary = yield* computedState.gen<DashboardSummary>('summary',
* ['users', 'orders', 'revenue'],
* (users, orders, revenue) => ({
* totalUsers: users.length,
* totalOrders: orders.length,
* averageOrderValue: revenue / orders.length
* })
* );
*
* yield* renderDashboard(summary);
* });
* ```
*/
computedState.gen = function <T>(
key: string,
dependencies: string[],
compute: (...deps: any[]) => T,
): Workflow<T> {
return computedState<T>(key, dependencies, compute);
};
/**
* Explicit generator version of watchState for guaranteed workflow behavior.
*
* This .gen version always returns a Workflow and provides explicit control
* over generator behavior for reactive state watching.
*
* @template T - Type of the state value being watched
* @param key - State key to watch for changes
* @param callback - Function called when state changes
* @returns Workflow<() => void> - Always returns a workflow for yield*
*
* @example Explicit generator usage
* ```typescript
* watch('.reactive-component', function* () {
* // Explicit .gen version - guaranteed workflow
* const cleanup = yield* watchState.gen<UserSettings>('settings', function* (newSettings) {
* yield* applySettings(newSettings);
* yield* saveSettings(newSettings);
* });
*
* yield* onUnmount(() => cleanup());
* });
* ```
*/
watchState.gen = function <T>(
key: string,
callback: (
newValue: T | undefined,
oldValue: T | undefined,
) => void | Generator<any, void, any>,
): Workflow<() => void> {
return watchState<T>(key, callback);
};
/**
* Explicit generator version of getStateSize for guaranteed workflow behavior.
*
* This .gen version always returns a Workflow and provides explicit control
* over generator behavior for state size retrieval.
*
* @returns Workflow<number> - Always returns a workflow for yield*
*
* @example Explicit generator usage
* ```typescript
* watch('.state-tracker', function* () {
* // Explicit .gen version - guaranteed workflow
* const size = yield* getStateSize.gen();
*
* yield* attr('.progress-bar', 'data-count', String(size));
* yield* toggleClass('has-state', size > 0);
* });
* ```
*/
getStateSize.gen = function (): Workflow<number> {
return getStateSize();
};
/**
* Merge state with an object, adding multiple key-value pairs at once.
*
* Takes an object and adds all its properties to the element's state map.
* Existing state values with the same keys will be overwritten.
*
* @param stateObject - Object containing key-value pairs to merge into state
* @returns Workflow<void> - Generator workflow for yield*
*
* @example Bulk state initialization
* ```typescript
* watch('.user-profile', function* () {
* const userData = {
* name: 'John Doe',
* email: 'john@example.com',
* preferences: { theme: 'dark', lang: 'en' },
* lastLogin: new Date(),
* isActive: true
* };
*
* yield* mergeState(userData);
*
* // Now all properties are available in state
* const name = yield* getState<string>('name');
* yield* text('.user-name', name);
* });
* ```
*
* @example Form data collection
* ```typescript
* watch('.contact-form', function* () {
* yield* submit(function* (event) {
* event.preventDefault();
*
* const formData = new FormData(event.target);
* const formObject = Object.fromEntries(formData.entries());
*
* // Merge all form data into state
* yield* mergeState(formObject);
*
* // Add metadata
* yield* mergeState({
* submittedAt: new Date(),
* isValid: true,
* attempts: (yield* getState<number>('attempts', 0)) + 1
* });
* });
* });
* ```
*
* @example Configuration merging
* ```typescript
* watch('.config-panel', function* () {
* const defaultConfig = {
* autoSave: true,
* theme: 'light',
* language: 'en',
* notifications: true
* };
*
* const userConfig = yield* loadUserConfig();
*
* // Merge default config first, then user overrides
* yield* mergeState(defaultConfig);
* yield* mergeState(userConfig);
* });
* ```
*/
export function mergeState(stateObject: Record<string, any>): Workflow<void> {
return (function* (): Generator<Operation<void>, void, any> {
yield ((context: WatchContext) => {
if (!context.state) {
(context as any).state = new Map();
}
Object.entries(stateObject).forEach(([key, value]) => {
context.state!.set(key, value);
});
}) as Operation<void>;
})();
}
/**
* Explicit generator version of mergeState for guaranteed workflow behavior.
*
* This .gen version always returns a Workflow and provides explicit control
* over generator behavior for bulk state merging.
*
* @param stateObject - Object containing key-value pairs to merge into state
* @returns Workflow<void> - Always returns a workflow for yield*
*
* @example Explicit generator usage
* ```typescript
* watch('.bulk-importer', function* () {
* yield* click('.import-data', function* () {
* const importData = yield* loadImportData();
*
* // Explicit .gen version - guaranteed workflow
* yield* mergeState.gen(importData);
*
* yield* addClass('data-imported');
* yield* text('.status', 'Data imported successfully');
* });
* });
* ```
*/
mergeState.gen = function (stateObject: Record<string, any>): Workflow<void> {
return mergeState(stateObject);
};
/**
* Get all state as a plain object with type safety.
*
* Converts the element's state map into a plain JavaScript object,
* making it easy to serialize, inspect, or pass to other functions.
*
* @template T - Expected type of the returned object (defaults to Record<string, any>)
* @returns Workflow<T> - Object containing all state key-value pairs
*
* @example State serialization
* ```typescript
* watch('.data-exporter', function* () {
* yield* click('.export', function* () {
* const stateObj = yield* getStateObject<UserState>();
*
* yield* attr('.download-link', 'href',
* `data:application/json,${encodeURIComponent(JSON.stringify(stateObj))}`
* );
* yield* attr('.download-link', 'download', 'user-state.json');
* });
* });
* ```
*
* @example State debugging
* ```typescript
* watch('.debug-panel', function* () {
* yield* click('.dump-state', function* () {
* const state = yield* getStateObject();
*
* console.log('Current state:', state);
* yield* text('.debug-output', JSON.stringify(state, null, 2));
* });
* });
* ```
*
* @example State validation with types
* ```typescript
* interface FormState {
* name: string;
* email: string;
* age: number;
* preferences: UserPreferences;
* }
*
* watch('.form-validator', function* () {
* yield* click('.validate-state', function* () {
* const formState = yield* getStateObject<FormState>();
*
* const isValid = validateFormState(formState);
* yield* toggleClass('valid-form', isValid);
*
* if (!isValid) {
* yield* text('.errors', 'Please complete all required fields');
* }
* });
* });
* ```
*/
export function getStateObject<
T extends Record<string, any> = Record<string, any>,
>(): Workflow<T> {
return (function* (): Generator<Operation<T>, T, any> {
const result = yield ((context: WatchContext) => {
if (!context.state) {
return {} as T;
}
const obj: any = {};
context.state.forEach((value, key) => {
obj[key] = value;
});
return obj as T;
}) as Operation<T>;
return result;
})();
}
/**
* Explicit generator version of getStateObject for guaranteed workflow behavior.
*
* This .gen version always returns a Workflow and provides explicit control
* over generator behavior for state object conversion.
*
* @template T - Expected type of the returned object
* @returns Workflow<T> - Always returns a workflow for yield*
*
* @example Explicit generator usage
* ```typescript
* watch('.state-processor', function* () {
* // Explicit .gen version - guaranteed workflow
* const stateObj = yield* getStateObject.gen<ApplicationState>();
*
* yield* processStateObject(stateObj);
* yield* updateUI(stateObj);
* });
* ```
*/
getStateObject.gen = function <
T extends Record<string, any> = Record<string, any>,
>(): Workflow<T> {
return getStateObject<T>();
};
/**
* Watch for changes to a specific state key with reactive callbacks.
*
* Sets up a reactive observer that triggers a callback whenever the specified
* state key is modified. The callback receives both the new and old values
* and can be either a regular function or a generator function.
*
* @template T - Type of the state value being watched
* @param key - State key to watch for changes
* @param callback - Function called when state changes (old value, new value)
* @returns Workflow<() => void> - Cleanup function to stop watching
*
* @example Basic state watching
* ```typescript
* watch('.counter', function* () {
* yield* setState('count', 0);
*
* const stopWatching = yield* watchState<number>('count', function* (newVal, oldVal) {
* yield* text('.display', `Count changed from ${oldVal} to ${newVal}`);
*
* if (newVal > 10) {
* yield* addClass('high-count');
* }
* });
*
* // Stop watching when component unmounts
* yield* onUnmount(() => stopWatching());
* });
* ```
*
* @example Form validation with state watching
* ```typescript
* watch('.form-field', function* () {
* yield* watchState<string>('value', function* (newValue, oldValue) {
* // Validate on every change
* const isValid = yield* validateField(newValue);
*
* yield* toggleClass('valid', isValid);
* yield* toggleClass('invalid', !isValid);
*
* if (!isValid) {
* yield* text('.error-message', 'This field is required');
* } else {
* yield* text('.error-message', '');
* }
* });
* });
* ```
*
* @example Complex state dependency tracking
* ```typescript
* watch('.shopping-cart', function* () {
* yield* watchState<CartItem[]>('items', function* (newItems, oldItems) {
* const total = newItems.reduce((sum, item) => sum + item.price * item.quantity, 0);
* yield* setState('total', total);
*
* yield* text('.item-count', `${newItems.length} items`);
* yield* text('.total-price', `$${total.toFixed(2)}`);
*
* // Update localStorage
* yield* persistState('items');
* });
* });
* ```
*/
export function watchState<T>(
key: string,
callback: (
newValue: T | undefined,
oldValue: T | undefined,
) => void | Generator<any, void, any>,
): Workflow<() => void> {
return (function* (): Generator<Operation<() => void>, () => void, any> {
const cleanup = yield ((context: WatchContext) => {
if (!context.state) {
(context as any).state = new Map();
}
// Store the current value
let currentValue = context.state.get(key) as T | undefined;
// Create a proxy or use Object.defineProperty to intercept set operations
// For simplicity, we'll use a polling approach or override the Map's set method
const originalSet = context.state.set.bind(context.state);
const originalDelete = context.state.delete.bind(context.state);
const originalClear = context.state.clear.bind(context.state);
// Override set method
context.state.set = function (k: string, v: any) {
if (k === key) {
const oldValue = currentValue;
currentValue = v as T;
const result = originalSet(k, v);
// Execute callback
const callbackResult = callback(currentValue, oldValue);
if (
callbackResult &&
typeof callbackResult === "object" &&
Symbol.iterator in callbackResult
) {
// Execute sync generator
const gen = callbackResult as Generator<any, void, any>;
let genResult = gen.next();
while (!genResult.done) {
if (typeof genResult.value === "function") {
genResult.value(context);
}
genResult = gen.next();
}
}
return result;
}
return originalSet(k, v);
};
// Override delete method
context.state.delete = function (k: string) {
if (k === key && context.state!.has(key)) {
const oldValue = currentValue;
currentValue = undefined;
const result = originalDelete(k);
// Execute callback
const callbackResult = callback(undefined, oldValue);
if (
callbackResult &&
typeof callbackResult === "object" &&
Symbol.iterator in callbackResult
) {
const gen = callbackResult as Generator<any, void, any>;
let genResult = gen.next();
while (!genResult.done) {
if (typeof genResult.value === "function") {
genResult.value(context);
}
genResult = gen.next();
}
}
return result;
}
return originalDelete(k);
};
// Return cleanup function
return () => {
// Restore original methods
context.state!.set = originalSet;
context.state!.delete = originalDelete;
context.state!.clear = originalClear;
};
}) as Operation<() => void>;
return cleanup;
})();
}
/**
* Create a computed state value that automatically updates based on dependencies.
*
* Creates a derived state value that is automatically computed from other state values.
* When any dependency changes, the computed value is recalculated and cached.
*
* @template T - Type of the computed value
* @param key - Key where the computed value will be cached
* @param dependencies - Array of state keys that this computation depends on
* @param compute - Function that computes the value from dependency values
* @returns Workflow<T> - The computed value
*
* @example Shopping cart total
* ```typescript
* watch('.shopping-cart', function* () {
* yield* setState('items', [
* { name: 'Widget', price: 10, quantity: 2 },
* { name: 'Gadget', price: 15, quantity: 1 }
* ]);
* yield* setState('taxRate', 0.08);
*
* // Computed subtotal
* const subtotal = yield* computedState<number>('subtotal', ['items'], (items) => {
* return items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
* });
*
* // Computed total with tax
* const total = yield* computedState<number>('total', ['subtotal', 'taxRate'], (subtotal, taxRate) => {
* return subtotal * (1 + taxRate);
* });
*
* yield* text('.subtotal', `Subtotal: $${subtotal.toFixed(2)}`);
* yield* text('.total', `Total: $${total.toFixed(2)}`);
* });
* ```
*
* @example Form validation status
* ```typescript
* watch('.registration-form', function* () {
* const isValid = yield* computedState<boolean>('formValid',
* ['name', 'email', 'password'],
* (name, email, password) => {
* return name && name.length >= 2 &&
* email && email.includes('@') &&
* password && password.length >= 8;
* }
* );
*
* yield* toggleClass('form-valid', isValid);
* yield* attr('.submit-btn', 'disabled', !isValid ? 'true' : null);
* });
* ```
*
* @example User display name
* ```typescript
* watch('.user-profile', function* () {
* yield* setState('firstName', 'John');
* yield* setState('lastName', 'Doe');
* yield* setState('username', 'johndoe123');
*
* const displayName = yield* computedState<string>('displayName',
* ['firstName', 'lastName', 'username'],
* (firstName, lastName, username) => {
* if (firstName && lastName) {
* return `${firstName} ${lastName}`;
* }
* return username || 'Anonymous';
* }
* );
*
* yield* text('.user-name', displayName);
* });
* ```
*/
export function computedState<T>(
key: string,
dependencies: string[],
compute: (...deps: any[]) => T,
): Workflow<T> {
return (function* (): Generator<Operation<T>, T, any> {
const result = yield ((context: WatchContext) => {
if (!context.state) {
(context as any).state = new Map();
}
// Get dependency values
const depValues = dependencies.map((dep) => context.state!.get(dep));
// Compute and cache the value
const computedValue = compute(...depValues);
context.state.set(`__computed_${key}`, computedValue);
return computedValue;
}) as Operation<T>;
return result;
})();
}
/**
* Persist a state value to localStorage for persistence across sessions.
*
* Saves the specified state value to localStorage using JSON serialization.
* The value will be available even after page refreshes or browser restarts.
*
* @param key - State key to persist
* @param storageKey - Optional custom localStorage key (defaults to watch_state_{key})
* @returns Workflow<void> - Generator workflow for yield*
*
* @example User preferences persistence
* ```typescript
* watch('.settings-panel', function* () {
* yield* change('.theme-selector', function* (e) {
* const theme = e.target.value;
* yield* setState('theme', theme);
* yield* persistState('theme', 'user_theme_preference');
*
* yield* addClass('theme-' + theme);
* });
* });
* ```
*
* @example Form draft saving
* ```typescript
* watch('.blog-editor', function* () {
* yield* input('.content-textarea', function* (e) {
* const content = e.target.value;
* yield* setState('draftContent', content);
*
* // Auto-save draft every few seconds
* yield* persistState('draftContent', 'blog_draft');
* }, { debounce: 2000 });
* });
* ```
*
* @example Shopping cart persistence
* ```typescript
* watch('.shopping-cart', function* () {
* yield* watchState<CartItem[]>('items', function* (newItems) {
* // Persist cart whenever items change
* yield* persistState('items', 'shopping_cart_items');
* yield* text('.save-status', 'Cart saved');
* });
* });
* ```
*/
export function persistState(key: string, storageKey?: string): Workflow<void> {
return (function* (): Generator<Operation<void>, void, any> {
yield ((context: WatchContext) => {
if (!context.state) {
return;
}
const value = context.state.get(key);
const actualStorageKey = storageKey || `watch_state_${key}`;
if (value !== undefined) {
try {
localStorage.setItem(actualStorageKey, JSON.stringify(value));
} catch (e) {
console.error("Failed to persist state:", e);
}
}
}) as Operation<void>;
})();
}
/**
* Restore a state value from localStorage with fallback defaults.
*
* Attempts to load and parse a value from localStorage and set it in state.
* If the value doesn't exist or parsing fails, uses the provided default value.
*
* @template T - Type of the value being restored
* @param key - State key to restore the value to
* @param storageKey - Optional custom localStorage key (defaults to watch_state_{key})
* @param defaultValue - Fallback value if localStorage doesn't contain the key
* @returns Workflow<T | undefined> - The restored value or undefined
*
* @example User preferences restoration
* ```typescript
* watch('.app-container', function* () {
* // Restore theme preference on app start
* const theme = yield* restoreState<string>('theme', 'user_theme_preference', 'light');
* yield* addClass('theme-' + theme);
*
* // Restore language preference
* const language = yield* restoreState<string>('language', 'user_language', 'en');
* yield* attr('html', 'lang', language);
* });
* ```
*
* @example Form draft restoration
* ```typescript
* watch('.blog-editor', function* () {
* // Restore draft content when editor loads
* const draftContent = yield* restoreState<string>('draftContent', 'blog_draft', '');
*
* if (draftContent) {
* yield* value('.content-textarea', draftContent);
* yield* addClass('has-draft');
* yield* text('.draft-indicator', 'Draft restored');
* }
* });
* ```
*
* @example Shopping cart restoration
* ```typescript
* watch('.shopping-cart', function* () {
* const savedItems = yield* restoreState<CartItem[]>('items', 'shopping_cart_items', []);
*
* if (savedItems.length > 0) {
* yield* renderCartItems(savedItems);
* yield* text('.cart-count', String(savedItems.length));
* }
* });
* ```
*/
export function restoreState<T>(
key: string,
storageKey?: string,
defaultValue?: T,
): Workflow<T | undefined> {
return (function* (): Generator<
Operation<T | undefined>,
T | undefined,
any
> {
const result = yield ((context: WatchContext) => {
if (!context.state) {
(context as any).state = new Map();
}
const actualStorageKey = storageKey || `watch_state_${key}`;
try {
const stored = localStorage.getItem(actualStorageKey);
if (stored !== null) {
const value = JSON.parse(stored) as T;
context.state.set(key, value);
return value;
}
} catch (e) {
console.error("Failed to restore state:", e);
}
if (defaul