Enhance storedSignal function to accept an Injector for improved context handling in localStorage persistence

This commit is contained in:
nessar committed 2025-11-06 23:59:26 +01:00
1 parent 275512d443
commit 7ffa57748b
2 files changed
+37 -23

No files matched your search

@@ -1,4 +1,4 @@
import { Injectable, WritableSignal } from '@angular/core';
import { Injectable, WritableSignal, inject, Injector } from '@angular/core';
import { storedSignal } from '../shared/utils/stored-signal';
interface SettingDefinition {
@@ -10,6 +10,8 @@ interface SettingDefinition {
providedIn: 'root',
})
export class CalendarSettingsService {
private readonly injector = inject(Injector);
private readonly definitions = new Map<string, SettingDefinition>([
[
'showWeekNumbers',
@@ -38,7 +40,7 @@ export class CalendarSettingsService {
constructor() {
for (const [id, def] of this.definitions) {
const storageKey = `calendarSettings.${id}`;
this.signals.set(id, storedSignal(storageKey, def.defaultValue));
this.signals.set(id, storedSignal(storageKey, def.defaultValue, this.injector));
}
this.settingsArray = Array.from(this.definitions.entries()).map(([id, def]) => ({
+33 -21
View File
@@ -1,4 +1,4 @@
import { signal, effect, WritableSignal } from '@angular/core';
import { signal, effect, WritableSignal, Injector, runInInjectionContext } from '@angular/core';
/**
* Checks if localStorage is available in the current environment.
@@ -24,24 +24,31 @@ function isLocalStorageAvailable(): boolean {
* @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 injector - The Angular injector (use inject(Injector) to get current context)
* @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);
* const injector = inject(Injector);
* const darkMode = storedSignal('theme.darkMode', false, injector);
* 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`)
* - Requires an Injector to create the effect in the proper context
* - 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> {
export function storedSignal<T>(
key: string,
initialValue: T,
injector: Injector,
debounceMs = 300,
): WritableSignal<T> {
let parsedValue = initialValue;
if (isLocalStorageAvailable()) {
@@ -67,25 +74,30 @@ export function storedSignal<T>(key: string, initialValue: T, debounceMs = 300):
let timeoutId: ReturnType<typeof setTimeout> | null = null;
effect(() => {
const value = s();
runInInjectionContext(injector, () => {
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);
}
// Clear any pending timeout
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
timeoutId = null;
}, debounceMs);
// 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;