Refactor calendar settings service to utilize a new storedSignal implementation for localStorage persistence and restore the stored-signal utility

This commit is contained in:
nessar committed 2025-11-06 23:40:18 +01:00
1 parent d70aef4962
commit b793019ba7
3 files changed
+102 -31

No files matched your search

+12 -13
View File
@@ -1,5 +1,5 @@
import { storedSignal } from '../shared/utils/stored-signal.';
import { Injectable, WritableSignal } from '@angular/core';
import { storedSignal } from '../shared/utils/stored-signal';
interface SettingDefinition {
defaultValue: boolean;
@@ -29,30 +29,29 @@ export class CalendarSettingsService {
private readonly signals = new Map<string, WritableSignal<boolean>>();
readonly settingsArray: ReadonlyArray<{
id: string;
signal: WritableSignal<boolean>;
description: string;
}>;
constructor() {
for (const [id, def] of this.definitions) {
const storageKey = `calendarSettings.${id}`;
this.signals.set(id, storedSignal(storageKey, def.defaultValue));
}
}
get(id: string): WritableSignal<boolean> {
const signal = this.signals.get(id);
if (signal) {
return signal;
}
// Return a default signal if the ID is invalid
return storedSignal(`calendarSettings.${id}`, false);
}
get settingsArray() {
return Array.from(this.definitions.entries()).map(([id, def]) => ({
this.settingsArray = Array.from(this.definitions.entries()).map(([id, def]) => ({
id,
signal: this.signals.get(id)!,
description: def.description,
}));
}
get(id: string): WritableSignal<boolean> {
return this.signals.get(id)!;
}
getDescription(id: string): string {
return this.definitions.get(id)?.description ?? '';
}
-18
View File
@@ -1,18 +0,0 @@
import { signal, effect, WritableSignal } from '@angular/core';
export function storedSignal<T>(key: string, initialValue: T): WritableSignal<T> {
// Try to restore the value from localStorage
const storedValue = localStorage.getItem(key);
const parsedValue = storedValue ? (JSON.parse(storedValue) as T) : initialValue;
// Create the actual signal
const s = signal(parsedValue);
// Keep localStorage in sync whenever the signal changes
effect(() => {
const value = s();
localStorage.setItem(key, JSON.stringify(value));
});
return s;
}
+90
View File
@@ -0,0 +1,90 @@
import { signal, effect, WritableSignal } from '@angular/core';
/**
* Checks if localStorage is available in the current environment.
* @returns true if localStorage is accessible, false otherwise
*/
function isLocalStorageAvailable(): boolean {
try {
return typeof localStorage !== 'undefined' && localStorage !== null;
} catch {
return false;
}
}
/**
* Creates a signal that automatically persists its value to localStorage.
*
* The signal will:
* - Initialize with the stored value from localStorage if available
* - Fall back to the provided initial value if no stored value exists or parsing fails
* - Automatically sync changes to localStorage with debouncing (300ms delay)
* - Work safely in SSR and environments where localStorage is unavailable
*
* @template T - The type of value stored in the signal (must be JSON-serializable)
* @param key - The localStorage key used to store the value
* @param initialValue - The default value used when no stored value exists
* @param debounceMs - Optional debounce delay in milliseconds (default: 300ms)
* @returns A writable signal that persists to localStorage
*
* @example
* ```typescript
* const darkMode = storedSignal('theme.darkMode', false);
* darkMode.set(true); // Automatically saved to localStorage after 300ms
* console.log(darkMode()); // true
* ```
*
* @remarks
* - Must be called within an injection context (uses Angular's `effect`)
* - Values must be JSON-serializable (no functions, circular references, etc.)
* - In SSR or when localStorage is unavailable, the signal works normally but won't persist
* - Corrupted localStorage data is automatically cleared and reset to initial value
* - localStorage writes are debounced to improve performance with frequent updates
*/
export function storedSignal<T>(key: string, initialValue: T, debounceMs = 300): WritableSignal<T> {
let parsedValue = initialValue;
if (isLocalStorageAvailable()) {
try {
const storedValue = localStorage.getItem(key);
if (storedValue) {
parsedValue = JSON.parse(storedValue) as T;
}
} catch (error) {
console.warn(
`Failed to parse stored value for key "${key}". Using initial value.`,
error,
);
try {
localStorage.removeItem(key);
} catch {}
}
}
const s = signal(parsedValue);
let timeoutId: ReturnType<typeof setTimeout> | null = null;
effect(() => {
const value = s();
// Clear any pending timeout
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
// Debounce the localStorage write
timeoutId = setTimeout(() => {
if (isLocalStorageAvailable()) {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.warn(`Failed to save value for key "${key}" to localStorage.`, error);
}
}
timeoutId = null;
}, debounceMs);
});
return s;
}