Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: handle defaultValue not undefined typing #508

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions docs/src/content/docs/utilities/Injectors/inject-local-storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,7 @@ Options to configure the behavior of the local storage signal.
Here's a basic example of using `injectLocalStorage`:

```typescript
const username = injectLocalStorage<string>('username', {
defaultValue: 'Anonymous',
storageSync: true,
});
const username = injectLocalStorage<string>('username');

username.set('John Doe');
username.update((username) => 'Guest ' + username);
Expand All @@ -67,4 +64,23 @@ effect(() => {
console.log(username());
});
// Use `username` in your component to get or set the username stored in local storage.
// The value might be null or undefined if default value is not provided.
```

Fallback value can be provided using the `defaultValue` option:

```typescript
const username = injectLocalStorage<string>('username', {
defaultValue: 'Guest',
});
// If the key 'username' is not present in local storage, the default value 'Guest' will be used.
```

Storage synchronization can be enabled using the `storageSync` option:

```typescript
const username = injectLocalStorage<string>('username', {
storageSync: true,
});
// Changes to the local storage will be reflected across browser tabs.
```
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,28 @@ describe('injectLocalStorage', () => {
});
}));

it('should get a undefined value from localStorage', fakeAsync(() => {
TestBed.runInInjectionContext(() => {
getItemSpy.mockReturnValue(undefined); // Mock return value for getItem
const localStorageSignal = injectLocalStorage<string>(key);
tick(); // Wait for effect to run
expect(localStorageSignal()).toBeUndefined();
});
}));

it('should return defaultValue of type string', () => {
TestBed.runInInjectionContext(() => {
getItemSpy.mockReturnValue(undefined); // Mock return value for getItem
const defaultValue = 'default';
const localStorageSignal = injectLocalStorage<string>(key, {
eneajaho marked this conversation as resolved.
Show resolved Hide resolved
defaultValue,
});

expect(typeof localStorageSignal()).not.toBeUndefined();
expect(localStorageSignal()).toEqual(defaultValue);
});
});

it('should get the current value from localStorage', () => {
TestBed.runInInjectionContext(() => {
const testValue = 'value';
Expand Down
63 changes: 50 additions & 13 deletions libs/ngxtension/inject-local-storage/src/inject-local-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,7 @@ export function provideLocalStorageImpl(impl: typeof globalThis.localStorage) {
/**
* Options to override the default behavior of the local storage signal.
*/
export type LocalStorageOptions<T> = {
/**
* The default value to use when the key is not present in local storage.
*/
defaultValue?: T | (() => T);
export type LocalStorageOptionsNoDefault = {
/**
*
* Determines if local storage syncs with the signal.
Expand All @@ -56,6 +52,25 @@ export type LocalStorageOptions<T> = {
injector?: Injector;
};

export type LocalStorageOptionsWithDefaultValue<T> =
LocalStorageOptionsNoDefault & {
/**
* Default value for the signal.
* Can be a value or a function that returns the value.
*/
defaultValue: T | (() => T);
};

export type LocalStorageOptions<T> =
| LocalStorageOptionsNoDefault
| LocalStorageOptionsWithDefaultValue<T>;

function isLocalStorageWithDefaultValue<T>(
options: LocalStorageOptions<T>,
): options is LocalStorageOptionsWithDefaultValue<T> {
return 'defaultValue' in options;
}

function isFunction(value: unknown): value is (...args: unknown[]) => unknown {
return typeof value === 'function';
}
Expand All @@ -72,28 +87,48 @@ function parseJSON(value: string): unknown {
return value === 'undefined' ? undefined : JSON.parse(value);
}

export const injectLocalStorage = <T>(
export const injectLocalStorage: {
<T>(
key: string,
options: LocalStorageOptionsWithDefaultValue<T>,
): WritableSignal<T>;
<T>(
key: string,
options?: LocalStorageOptionsNoDefault,
): WritableSignal<T | undefined>;
} = <T>(
key: string,
options: LocalStorageOptions<T> = {},
): WritableSignal<T | undefined> => {
const defaultValue = isFunction(options.defaultValue)
? options.defaultValue()
: options.defaultValue;
if (isLocalStorageWithDefaultValue(options)) {
const defaultValue = isFunction(options.defaultValue)
? options.defaultValue()
: options.defaultValue;

return internalInjectLocalStorage<T>(key, options, defaultValue);
}
return internalInjectLocalStorage<T | undefined>(key, options, undefined);
};

const internalInjectLocalStorage = <R>(
key: string,
options: LocalStorageOptions<R>,
defaultValue: R,
): WritableSignal<R> => {
const stringify = isFunction(options.stringify)
? options.stringify
: JSON.stringify;
const parse = isFunction(options.parse) ? options.parse : parseJSON;
const storageSync = options.storageSync ?? true;

return assertInjector(injectLocalStorage, options.injector, () => {
const localStorage = inject(NGXTENSION_LOCAL_STORAGE);
const destroyRef = inject(DestroyRef);

const initialStoredValue = goodTry(() => localStorage.getItem(key));
const initialValue = initialStoredValue
? goodTry(() => parse(initialStoredValue) as T)
? goodTry(() => parse(initialStoredValue) as R) ?? defaultValue
: defaultValue;
const internalSignal = signal<T | undefined>(initialValue);
const internalSignal = signal(initialValue);

effect(() => {
const value = internalSignal();
Expand All @@ -108,7 +143,9 @@ export const injectLocalStorage = <T>(
const onStorage = (event: StorageEvent) => {
if (event.storageArea === localStorage && event.key === key) {
const newValue =
event.newValue !== null ? (parse(event.newValue) as T) : undefined;
event.newValue !== null
? (parse(event.newValue) as R)
: defaultValue;
internalSignal.set(newValue);
}
};
Expand Down
Loading