ngxtension / ngxtension-platform

Utilities for Angular
https://ngxtension.netlify.app/
MIT License
576 stars 83 forks source link

utility function proposal: syncWithLocalStorage #245

Closed zip-fa closed 1 month ago

zip-fa commented 7 months ago

Hi. I have little utility-function to sync my signal properties with local storage, which probably could be added to ngextension.

local-storage.ts:

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class LocalStorage {
  get isSupported(): boolean {
    return typeof localStorage !== "undefined";
  }

  setItem<T>(key: string, value: T): void {
    if (!this.isSupported) {
      return;
    }

    try {
      localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      if (
        error &&
        ((error as any).name === "QuotaExceededError" ||
          (error as any).name === "NS_ERROR_DOM_QUOTA_REACHED")
      ) {
        console.error(`The ${key} value exceeds the browser storage quota`);
      } else {
        console.error(
          `Error ocurred while serializing the ${key} store value, value not updated`
        );
      }
    }
  }

  getItem<T>(key: string): Partial<T> | null {
    if (!this.isSupported) {
      return null;
    }

    const storedValue = localStorage.getItem(key);

    if (storedValue === "undefined" || storedValue === null) {
      return null;
    }

    try {
      return JSON.parse(storedValue) as T;
    } catch {
      return null;
    }
  }
}

sync-with-local-storage.ts:

export function syncWithLocalStorage(storageKey: string, keysToSignals: Record<string, WritableSignal<any>>): void {
  assertInInjectionContext(syncWithLocalStorage);

  const localStorage = inject(LocalStorage);

  if (!localStorage.isSupported) {
    return null;
  }

  const data = localStorage.getItem<Record<string, any>>(storageKey);

  if (data) {
    for (const key in keysToSignals) {
      const value = data[key];

      if (value !== undefined) {
        keysToSignals[key].set(value);
      }
    }
  }

  effect(() => {
    const storageData: Record<string, WritableSignal<any>> = {};

    for (const key in keysToSignals) {
      storageData[key] = keysToSignals[key]();
    }

    localStorage.setItem(storageKey, storageData);
  });
}

usage:

test = signal<string>('test'); // initial value; will be overwriten with value from localStorage

// call it from here
private localStorageSync = syncWithLocalStorage('testNamespace', {
  test: this.test
});

// or from constructor
constructor() {
  syncWithLocalStorage('testNamespace', {
    test: this.test
  });
}

It will:

1) create testNamespace key in localstorage with this serialized json structure: {"test": "123"} 2) get value from localStorage & set it to signal on class init phase (if exists) 3) trigger on every signal change & update local storage 4) not throw error in SSR env

eneajaho commented 7 months ago

Hi, I was also thinking of having something like this one https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event

When the storage changes, we also emit that value back to the signal

zip-fa commented 7 months ago

Yes, this would be helpful to "notify changes" from another tab and synchronise signals state between tabs with zero efforts

palexcast commented 6 months ago

@zip-fa I opened a PR https://github.com/nartc/ngxtension-platform/pull/295 for this. It's a bit different from what you proposed, but it is similar to an implementation I had created for something else.

Would love to get your feedback on both implementation and ease of use.

alcaidio commented 5 months ago

Maybe this post will be useful to have local storage type safe ;) https://blog.herodevs.com/interact-with-browser-storage-type-safe-fee0ad07428f

eneajaho commented 1 month ago

Closing this as already merged https://github.com/ngxtension/ngxtension-platform/blob/main/libs/ngxtension/inject-local-storage/src/inject-local-storage.ts

We just need docs for it now.