Refactor calendar settings service to utilize a new storedSignal implementation for localStorage persistence and restore the stored-signal utility
This commit is contained in:
1 parent
d70aef4962
commit
b793019ba7
3 files changed
+102
-31
No files matched your search
@@ -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 ?? '';
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user