Skip to content

Commit 07073ee

Browse files
committed
feat(forms-manager): add support for configurable browser storage
Adds ability to use sessionStorage or other custom storage solution for persistence.
1 parent e87ff6f commit 07073ee

File tree

7 files changed

+140
-67
lines changed

7 files changed

+140
-67
lines changed

README.md

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -246,17 +246,64 @@ formsManager.destroy();
246246
formsManager.controlChanges('login').pipe(takeUntil(controlDestroyed('login')));
247247
```
248248

249-
## Persist to Local Storage or Session Storage
249+
## Persist to browser storage (localStorage, sessionStorage or custom storage solution)
250250

251251
In the `upsert` method, pass the `persistState` flag:
252252

253253
```ts
254254
formsManager.upsert(formName, abstractContorl, {
255-
persistState: true;
255+
persistState: true,
256256
});
257257
```
258258

259-
By default, the state is persisted to Local Storage. Session Storage can be used instead by passing the `NG_FORMS_MANAGER_CONFIG` provider (see below).
259+
By default, the state is persisted to `localStorage` ([Link](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage)).
260+
261+
For storage to `sessionStorage` ([Link](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage)), add `FORMS_MANAGER_SESSION_STORAGE_PROVIDER` to the providers array in `app.module.ts`:
262+
263+
```ts
264+
import { FORMS_MANAGER_SESSION_STORAGE_PROVIDER } from '@ngneat/forms-manager';
265+
266+
@NgModule({
267+
declarations: [AppComponent],
268+
imports: [ ... ],
269+
providers: [
270+
...
271+
FORMS_MANAGER_SESSION_STORAGE_PROVIDER,
272+
...
273+
],
274+
bootstrap: [AppComponent],
275+
})
276+
export class AppModule {}
277+
```
278+
279+
Furthermore, a **custom storage provider**, which must implement the `Storage` interface ([Link](https://developer.mozilla.org/en-US/docs/Web/API/Storage)) can be provided through the `FORMS_MANAGER_STORAGE` token:
280+
281+
```ts
282+
import { FORMS_MANAGER_STORAGE } from '@ngneat/forms-manager';
283+
284+
class MyStorage implements Storage {
285+
public clear() { ... }
286+
public key(index: number): string | null { ... }
287+
public getItem(key: string): string | null { ... }
288+
public removeItem(key: string) { ... }
289+
public setItem(key: string, value: string) { ... }
290+
}
291+
292+
@NgModule({
293+
declarations: [AppComponent],
294+
imports: [ ... ],
295+
providers: [
296+
...
297+
{
298+
provide: FORMS_MANAGER_STORAGE,
299+
useValue: MyStorage,
300+
},
301+
...
302+
],
303+
bootstrap: [AppComponent],
304+
})
305+
export class AppModule {}
306+
```
260307

261308
## Validators
262309

@@ -428,7 +475,6 @@ import { NG_FORMS_MANAGER_CONFIG, NgFormsManagerConfig } from '@ngneat/forms-man
428475
useValue: new NgFormsManagerConfig({
429476
debounceTime: 1000, // defaults to 300
430477
storage: {
431-
type: 'SessionStorage',
432478
key: 'NgFormManager',
433479
},
434480
}),

projects/ngneat/forms-manager/src/lib/config.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,23 @@
11
import { InjectionToken } from '@angular/core';
22

3-
type DeepPartial<T> = {
4-
[P in keyof T]?: DeepPartial<T[P]>;
5-
};
6-
7-
export type StorageOption = 'LocalStorage' | 'SessionStorage';
83
export type Config = {
94
storage: {
10-
type: StorageOption;
115
key: string;
126
};
137
debounceTime: number;
148
};
159

1610
const defaults: Config = {
1711
storage: {
18-
type: 'LocalStorage',
1912
key: 'ngFormsManager',
2013
},
2114
debounceTime: 300,
2215
};
2316

2417
export function mergeConfig(
2518
defaults: Config,
26-
providerConfig: DeepPartial<Config> = {},
27-
inlineConfig: DeepPartial<Config>
19+
providerConfig: Partial<Config> = {},
20+
inlineConfig: Partial<Config>
2821
): Config {
2922
return {
3023
...defaults,
@@ -39,9 +32,9 @@ export function mergeConfig(
3932
}
4033

4134
export class NgFormsManagerConfig {
42-
constructor(private config: DeepPartial<Config> = {}) {}
35+
constructor(private config: Partial<Config> = {}) {}
4336

44-
merge(inline: DeepPartial<Config> = {}): Config {
37+
merge(inline: Partial<Config> = {}): Config {
4538
return mergeConfig(defaults, this.config, inline);
4639
}
4740
}

projects/ngneat/forms-manager/src/lib/forms-manager.spec.ts

Lines changed: 31 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
22
import { FormArray, FormControl, FormGroup, Validators } from '@angular/forms';
33
import { NgFormsManager } from './forms-manager';
4-
import { NG_FORMS_MANAGER_CONFIG, NgFormsManagerConfig } from './config';
5-
import { LOCAL_STORAGE_TOKEN, SESSION_STORAGE_TOKEN } from './injection-tokens';
4+
import { NgFormsManagerConfig } from './config';
5+
import { FORMS_MANAGER_STORAGE, LOCAL_STORAGE_TOKEN } from './injection-tokens';
6+
import { Provider } from '@angular/core';
67

78
// get forms snapshot
89
function getSnapshot(formsManager) {
@@ -1449,58 +1450,48 @@ describe('FormsManager', () => {
14491450

14501451
describe('Storage', () => {
14511452
let formsManager: NgFormsManager;
1452-
let localStorageMock: jasmine.SpyObj<Storage>;
1453-
let sessionStorageMock: jasmine.SpyObj<Storage>;
1454-
1455-
function configureTestingModule(ngFormsManagerConfig: NgFormsManagerConfig) {
1453+
let localStorageMock: jasmine.SpyObj<Storage> = jasmine.createSpyObj('localStorage', [
1454+
'setItem',
1455+
'getItem',
1456+
]);
1457+
let customStorageMock: jasmine.SpyObj<Storage> = jasmine.createSpyObj('customStorage', [
1458+
'setItem',
1459+
'getItem',
1460+
]);
1461+
1462+
function configureTestingModule(providers: Array<Provider>) {
14561463
TestBed.configureTestingModule({
1457-
providers: [
1458-
NgFormsManager,
1459-
{
1460-
provide: NG_FORMS_MANAGER_CONFIG,
1461-
useValue: ngFormsManagerConfig,
1462-
},
1463-
{
1464-
provide: LOCAL_STORAGE_TOKEN,
1465-
useValue: jasmine.createSpyObj('localStorage', ['setItem', 'getItem']),
1466-
},
1467-
{
1468-
provide: SESSION_STORAGE_TOKEN,
1469-
useValue: jasmine.createSpyObj('sessionStorage', ['setItem', 'getItem']),
1470-
},
1471-
],
1464+
providers: [NgFormsManager, ...providers],
14721465
});
14731466
formsManager = TestBed.inject(NgFormsManager);
1474-
localStorageMock = TestBed.inject(LOCAL_STORAGE_TOKEN) as jasmine.SpyObj<Storage>;
1475-
sessionStorageMock = TestBed.inject(SESSION_STORAGE_TOKEN) as jasmine.SpyObj<Storage>;
14761467

14771468
formsManager.upsert('user', new FormGroup({ control: new FormControl('control') }), {
14781469
persistState: true,
14791470
});
14801471
}
14811472

1482-
it('should store to localStorage when storage parameter omitted in NG_FORMS_MANAGER_CONFIG', () => {
1483-
configureTestingModule(new NgFormsManagerConfig());
1484-
expect(localStorageMock.getItem).toHaveBeenCalled();
1485-
expect(localStorageMock.setItem).toHaveBeenCalled();
1486-
expect(sessionStorageMock.getItem).not.toHaveBeenCalled();
1487-
expect(sessionStorageMock.setItem).not.toHaveBeenCalled();
1488-
});
1473+
it('should store to localStorage (by default) when FORMS_MANAGER_STORAGE not provided', () => {
1474+
configureTestingModule([
1475+
{
1476+
provide: LOCAL_STORAGE_TOKEN,
1477+
useValue: localStorageMock,
1478+
},
1479+
]);
14891480

1490-
it('should store to localStorage when configured as such in NG_FORMS_MANAGER_CONFIG', () => {
1491-
configureTestingModule(new NgFormsManagerConfig({ storage: { type: 'LocalStorage' } }));
14921481
expect(localStorageMock.getItem).toHaveBeenCalled();
14931482
expect(localStorageMock.setItem).toHaveBeenCalled();
1494-
expect(sessionStorageMock.getItem).not.toHaveBeenCalled();
1495-
expect(sessionStorageMock.setItem).not.toHaveBeenCalled();
14961483
});
14971484

1498-
it('should store to sessionStorage when configured as such in NG_FORMS_MANAGER_CONFIG', () => {
1499-
configureTestingModule(new NgFormsManagerConfig({ storage: { type: 'SessionStorage' } }));
1500-
expect(sessionStorageMock.getItem).toHaveBeenCalled();
1501-
expect(sessionStorageMock.setItem).toHaveBeenCalled();
1502-
expect(localStorageMock.getItem).not.toHaveBeenCalled();
1503-
expect(localStorageMock.setItem).not.toHaveBeenCalled();
1485+
it('should store to custom storage, provided through FORMS_MANAGER_STORAGE', () => {
1486+
configureTestingModule([
1487+
{
1488+
provide: FORMS_MANAGER_STORAGE,
1489+
useValue: customStorageMock,
1490+
},
1491+
]);
1492+
1493+
expect(customStorageMock.getItem).toHaveBeenCalled();
1494+
expect(customStorageMock.setItem).toHaveBeenCalled();
15041495
});
15051496

15061497
afterEach(() => {

projects/ngneat/forms-manager/src/lib/forms-manager.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,29 @@ import { AbstractControl, FormArray, FormGroup } from '@angular/forms';
33
import { EMPTY, merge, Observable, Subject, Subscription, timer } from 'rxjs';
44
import { debounce, distinctUntilChanged, filter, map, mapTo } from 'rxjs/operators';
55
import { deleteControl, findControl, handleFormArray, toStore } from './builders';
6-
import { Config, NG_FORMS_MANAGER_CONFIG, NgFormsManagerConfig } from './config';
6+
import { Config, NgFormsManagerConfig, NG_FORMS_MANAGER_CONFIG } from './config';
77
import { FormsStore } from './forms-manager.store';
88
import { isEqual } from './isEqual';
99
import { Control, ControlFactory, FormKeys, HashMap, UpsertConfig } from './types';
1010
import { coerceArray, filterControlKeys, filterNil, isBrowser, mergeDeep } from './utils';
11-
import { LOCAL_STORAGE_TOKEN, SESSION_STORAGE_TOKEN } from './injection-tokens';
11+
import { FORMS_MANAGER_STORAGE } from './injection-tokens';
1212

1313
const NO_DEBOUNCE = Symbol('NO_DEBOUNCE');
1414

1515
// @dynamic; see https://angular.io/guide/angular-compiler-options#strictmetadataemit
1616
@Injectable({ providedIn: 'root' })
1717
export class NgFormsManager<FormsState = any> {
1818
private readonly store: FormsStore<FormsState>;
19-
private readonly browserStorage?: Storage;
2019
private valueChanges$$: Map<keyof FormsState, Subscription> = new Map();
2120
private instances$$: Map<keyof FormsState, AbstractControl> = new Map();
2221
private initialValues$$: Map<keyof FormsState, any> = new Map();
2322
private destroy$$ = new Subject();
2423

2524
constructor(
2625
@Optional() @Inject(NG_FORMS_MANAGER_CONFIG) private config: NgFormsManagerConfig,
27-
@Optional() @Inject(LOCAL_STORAGE_TOKEN) private readonly localStorage?: Storage,
28-
@Optional() @Inject(SESSION_STORAGE_TOKEN) private readonly sessionStorage?: Storage
26+
@Optional() @Inject(FORMS_MANAGER_STORAGE) private readonly browserStorage?: Storage
2927
) {
3028
this.store = new FormsStore({} as FormsState);
31-
this.browserStorage =
32-
this.config.merge().storage.type === 'LocalStorage' ? this.localStorage : this.sessionStorage;
3329
}
3430

3531
/**
@@ -499,7 +495,7 @@ export class NgFormsManager<FormsState = any> {
499495
*
500496
* @example
501497
*
502-
* Removes the control from the store and from LocalStorage/SessionStorage
498+
* Removes the control from the store and from browser storage
503499
*
504500
* manager.clear('login');
505501
*

projects/ngneat/forms-manager/src/lib/injection-tokens.spec.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { TestBed } from '@angular/core/testing';
22
import { PLATFORM_ID } from '@angular/core';
3-
import { LOCAL_STORAGE_TOKEN, SESSION_STORAGE_TOKEN } from './injection-tokens';
3+
import {
4+
FORMS_MANAGER_SESSION_STORAGE_PROVIDER,
5+
FORMS_MANAGER_STORAGE,
6+
LOCAL_STORAGE_TOKEN,
7+
SESSION_STORAGE_TOKEN,
8+
} from './injection-tokens';
49

510
const PLATFORM_BROWSER_ID: string = 'browser';
611
const PLATFORM_SERVER_ID: string = 'server';
@@ -60,3 +65,17 @@ describe('LOCAL_STORAGE_TOKEN', () => {
6065
expect(TestBed.inject(LOCAL_STORAGE_TOKEN)).toBeUndefined();
6166
});
6267
});
68+
69+
describe('FORMS_MANAGER_STORAGE', () => {
70+
it('should contain the LOCAL_STORAGE_TOKEN by default', () => {
71+
TestBed.configureTestingModule({});
72+
expect(TestBed.inject(FORMS_MANAGER_STORAGE)).toBe(TestBed.inject(LOCAL_STORAGE_TOKEN));
73+
});
74+
});
75+
76+
describe('FORMS_MANAGER_SESSION_STORAGE_PROVIDER', () => {
77+
it('should provide SESSION_STORAGE_TOKEN', () => {
78+
TestBed.configureTestingModule({ providers: [FORMS_MANAGER_SESSION_STORAGE_PROVIDER] });
79+
expect(TestBed.inject(FORMS_MANAGER_STORAGE)).toBe(TestBed.inject(SESSION_STORAGE_TOKEN));
80+
});
81+
});
Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { isPlatformBrowser } from '@angular/common';
2-
import { InjectionToken, PLATFORM_ID, inject } from '@angular/core';
2+
import { inject, InjectionToken, PLATFORM_ID, Provider } from '@angular/core';
33

44
/**
5-
* Injection Token to safely inject {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage} to Angular DI
5+
* Injection Token to safely inject
6+
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage} to Angular DI
67
*/
78
export const SESSION_STORAGE_TOKEN: InjectionToken<Storage | undefined> = new InjectionToken<
89
Storage | undefined
@@ -12,11 +13,34 @@ export const SESSION_STORAGE_TOKEN: InjectionToken<Storage | undefined> = new In
1213
});
1314

1415
/**
15-
* Injection Token to safely inject {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage} to Angular DI
16+
* Injection Token to safely inject
17+
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage} to Angular DI
1618
*/
1719
export const LOCAL_STORAGE_TOKEN: InjectionToken<Storage | undefined> = new InjectionToken<
1820
Storage | undefined
1921
>('LOCAL_STORAGE_TOKEN', {
2022
providedIn: 'root',
2123
factory: () => (isPlatformBrowser(inject(PLATFORM_ID)) ? localStorage : undefined),
2224
});
25+
26+
/**
27+
* Injection Token to inject custom storage approach for persistence; must implement
28+
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Storage} interface
29+
*/
30+
export const FORMS_MANAGER_STORAGE = new InjectionToken<Storage | undefined>(
31+
'FORMS_MANAGER_STORAGE',
32+
{
33+
providedIn: 'root',
34+
factory: () =>
35+
isPlatformBrowser(inject(PLATFORM_ID)) ? inject(LOCAL_STORAGE_TOKEN) : undefined,
36+
}
37+
);
38+
39+
/**
40+
* Value provider that injects usage of
41+
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage} for persistence
42+
*/
43+
export const FORMS_MANAGER_SESSION_STORAGE_PROVIDER: Provider = {
44+
provide: FORMS_MANAGER_STORAGE,
45+
useExisting: SESSION_STORAGE_TOKEN,
46+
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
export { NgFormsManager } from './lib/forms-manager';
22
export { setAsyncValidators, setValidators } from './lib/validators';
33
export { NgFormsManagerConfig, NG_FORMS_MANAGER_CONFIG } from './lib/config';
4+
export {
5+
FORMS_MANAGER_STORAGE,
6+
FORMS_MANAGER_SESSION_STORAGE_PROVIDER,
7+
} from './lib/injection-tokens';

0 commit comments

Comments
 (0)