Skip to content

Commit c5400ba

Browse files
authored
Merge pull request #32 from nickhh76/feature/session-storage
2 parents dd1c627 + f468036 commit c5400ba

File tree

7 files changed

+278
-14
lines changed

7 files changed

+278
-14
lines changed

README.md

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

249-
## Persist to Local 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 `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+
```
307+
259308
## Validators
260309

261310
The library exposes two helpers method for adding cross component validation:
@@ -335,7 +384,7 @@ interface AppForms {
335384
name: string;
336385
age: number;
337386
city: string;
338-
}
387+
};
339388
}
340389
```
341390

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,20 @@ const defaults: Config = {
1515
};
1616

1717
export function mergeConfig(
18-
defaults: Partial<Config>,
18+
defaults: Config,
1919
providerConfig: Partial<Config> = {},
2020
inlineConfig: Partial<Config>
21-
) {
21+
): Config {
2222
return {
2323
...defaults,
24+
...providerConfig,
25+
...inlineConfig,
2426
storage: {
2527
...defaults.storage,
2628
...providerConfig.storage,
2729
...inlineConfig.storage,
2830
},
29-
...providerConfig,
30-
...inlineConfig,
31-
} as Config;
31+
};
3232
}
3333

3434
export class NgFormsManagerConfig {

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

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1-
import { fakeAsync, tick } from '@angular/core/testing';
1+
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
22
import { FormArray, FormControl, FormGroup, Validators } from '@angular/forms';
33
import { NgFormsManager } from './forms-manager';
44
import { NgFormsManagerConfig } from './config';
5+
import {
6+
FORMS_MANAGER_SESSION_STORAGE_PROVIDER,
7+
FORMS_MANAGER_STORAGE,
8+
LOCAL_STORAGE_TOKEN,
9+
SESSION_STORAGE_TOKEN,
10+
} from './injection-tokens';
11+
import { Provider } from '@angular/core';
512

613
// get forms snapshot
714
function getSnapshot(formsManager) {
@@ -1445,4 +1452,73 @@ describe('FormsManager', () => {
14451452
formsManager = null;
14461453
});
14471454
});
1455+
1456+
describe('Storage', () => {
1457+
let formsManager: NgFormsManager;
1458+
let localStorageMock: jasmine.SpyObj<Storage> = jasmine.createSpyObj('localStorage', [
1459+
'setItem',
1460+
'getItem',
1461+
]);
1462+
let sessionStorageMock: jasmine.SpyObj<Storage> = jasmine.createSpyObj('sessionStorage', [
1463+
'setItem',
1464+
'getItem',
1465+
]);
1466+
let customStorageMock: jasmine.SpyObj<Storage> = jasmine.createSpyObj('customStorage', [
1467+
'setItem',
1468+
'getItem',
1469+
]);
1470+
1471+
function configureTestingModule(providers: Array<Provider>) {
1472+
TestBed.configureTestingModule({
1473+
providers: [NgFormsManager, ...providers],
1474+
});
1475+
formsManager = TestBed.inject(NgFormsManager);
1476+
1477+
formsManager.upsert('user', new FormGroup({ control: new FormControl('control') }), {
1478+
persistState: true,
1479+
});
1480+
}
1481+
1482+
it('should store to localStorage (by default) when FORMS_MANAGER_STORAGE not provided', () => {
1483+
configureTestingModule([
1484+
{
1485+
provide: LOCAL_STORAGE_TOKEN,
1486+
useValue: localStorageMock,
1487+
},
1488+
]);
1489+
1490+
expect(localStorageMock.getItem).toHaveBeenCalled();
1491+
expect(localStorageMock.setItem).toHaveBeenCalled();
1492+
});
1493+
1494+
it('should store to sessionStorage when FORMS_MANAGER_SESSION_STORAGE_PROVIDER used', () => {
1495+
configureTestingModule([
1496+
{
1497+
provide: SESSION_STORAGE_TOKEN,
1498+
useValue: sessionStorageMock,
1499+
},
1500+
FORMS_MANAGER_SESSION_STORAGE_PROVIDER,
1501+
]);
1502+
1503+
expect(sessionStorageMock.getItem).toHaveBeenCalled();
1504+
expect(sessionStorageMock.setItem).toHaveBeenCalled();
1505+
});
1506+
1507+
it('should store to custom storage, provided through FORMS_MANAGER_STORAGE', () => {
1508+
configureTestingModule([
1509+
{
1510+
provide: FORMS_MANAGER_STORAGE,
1511+
useValue: customStorageMock,
1512+
},
1513+
]);
1514+
1515+
expect(customStorageMock.getItem).toHaveBeenCalled();
1516+
expect(customStorageMock.setItem).toHaveBeenCalled();
1517+
});
1518+
1519+
afterEach(() => {
1520+
formsManager.unsubscribe();
1521+
formsManager = null;
1522+
});
1523+
});
14481524
});

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ 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 { FORMS_MANAGER_STORAGE } from './injection-tokens';
1112

1213
const NO_DEBOUNCE = Symbol('NO_DEBOUNCE');
1314

15+
// @dynamic; see https://angular.io/guide/angular-compiler-options#strictmetadataemit
1416
@Injectable({ providedIn: 'root' })
1517
export class NgFormsManager<FormsState = any> {
1618
private readonly store: FormsStore<FormsState>;
@@ -19,7 +21,10 @@ export class NgFormsManager<FormsState = any> {
1921
private initialValues$$: Map<keyof FormsState, any> = new Map();
2022
private destroy$$ = new Subject();
2123

22-
constructor(@Optional() @Inject(NG_FORMS_MANAGER_CONFIG) private config: NgFormsManagerConfig) {
24+
constructor(
25+
@Optional() @Inject(NG_FORMS_MANAGER_CONFIG) private config: NgFormsManagerConfig,
26+
@Inject(FORMS_MANAGER_STORAGE) private readonly browserStorage?: Storage
27+
) {
2328
this.store = new FormsStore({} as FormsState);
2429
}
2530

@@ -490,7 +495,7 @@ export class NgFormsManager<FormsState = any> {
490495
*
491496
* @example
492497
*
493-
* Removes the control from the store and from LocalStorage
498+
* Removes the control from the store and from browser storage
494499
*
495500
* manager.clear('login');
496501
*
@@ -593,19 +598,22 @@ export class NgFormsManager<FormsState = any> {
593598
}
594599

595600
private removeFromStorage() {
596-
localStorage.setItem(this.config.merge().storage.key, JSON.stringify(this.store.getValue()));
601+
this.browserStorage?.setItem(
602+
this.config.merge().storage.key,
603+
JSON.stringify(this.store.getValue())
604+
);
597605
}
598606

599607
private updateStorage(name: keyof FormsState, value: any, config) {
600608
if (isBrowser() && config.persistState) {
601609
const storageValue = this.getFromStorage(config.storage.key);
602610
storageValue[name] = filterControlKeys(value);
603-
localStorage.setItem(config.storage.key, JSON.stringify(storageValue));
611+
this.browserStorage?.setItem(config.storage.key, JSON.stringify(storageValue));
604612
}
605613
}
606614

607615
private getFromStorage(key: string) {
608-
return JSON.parse(localStorage.getItem(key) || '{}');
616+
return JSON.parse(this.browserStorage?.getItem(key) || '{}');
609617
}
610618

611619
private deleteControl(name: FormKeys<FormsState>) {
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { TestBed } from '@angular/core/testing';
2+
import { PLATFORM_ID } from '@angular/core';
3+
import {
4+
FORMS_MANAGER_SESSION_STORAGE_PROVIDER,
5+
FORMS_MANAGER_STORAGE,
6+
LOCAL_STORAGE_TOKEN,
7+
SESSION_STORAGE_TOKEN,
8+
} from './injection-tokens';
9+
10+
const PLATFORM_BROWSER_ID: string = 'browser';
11+
const PLATFORM_SERVER_ID: string = 'server';
12+
const PLATFORM_WORKER_APP_ID: string = 'browserWorkerApp';
13+
const PLATFORM_WORKER_UI_ID: string = 'browserWorkerUi';
14+
15+
describe('SESSION_STORAGE_TOKEN', () => {
16+
it('should contain the sessionStorage object on platform browser', () => {
17+
TestBed.configureTestingModule({
18+
providers: [{ provide: PLATFORM_ID, useValue: PLATFORM_BROWSER_ID }],
19+
});
20+
expect(TestBed.inject(SESSION_STORAGE_TOKEN)).toBe(sessionStorage);
21+
});
22+
it('should contain undefined on platform server', () => {
23+
TestBed.configureTestingModule({
24+
providers: [{ provide: PLATFORM_ID, useValue: PLATFORM_SERVER_ID }],
25+
});
26+
expect(TestBed.inject(SESSION_STORAGE_TOKEN)).toBeUndefined();
27+
});
28+
it('should contain undefined on platform worker app', () => {
29+
TestBed.configureTestingModule({
30+
providers: [{ provide: PLATFORM_ID, useValue: PLATFORM_WORKER_APP_ID }],
31+
});
32+
expect(TestBed.inject(SESSION_STORAGE_TOKEN)).toBeUndefined();
33+
});
34+
it('should contain undefined on platform worker ui', () => {
35+
TestBed.configureTestingModule({
36+
providers: [{ provide: PLATFORM_ID, useValue: PLATFORM_WORKER_UI_ID }],
37+
});
38+
expect(TestBed.inject(SESSION_STORAGE_TOKEN)).toBeUndefined();
39+
});
40+
});
41+
42+
describe('LOCAL_STORAGE_TOKEN', () => {
43+
it('should contain the localStorage object on platform browser', () => {
44+
TestBed.configureTestingModule({
45+
providers: [{ provide: PLATFORM_ID, useValue: PLATFORM_BROWSER_ID }],
46+
});
47+
expect(TestBed.inject(LOCAL_STORAGE_TOKEN)).toBe(localStorage);
48+
});
49+
it('should contain undefined on platform server', () => {
50+
TestBed.configureTestingModule({
51+
providers: [{ provide: PLATFORM_ID, useValue: PLATFORM_SERVER_ID }],
52+
});
53+
expect(TestBed.inject(LOCAL_STORAGE_TOKEN)).toBeUndefined();
54+
});
55+
it('should contain undefined on platform worker app', () => {
56+
TestBed.configureTestingModule({
57+
providers: [{ provide: PLATFORM_ID, useValue: PLATFORM_WORKER_APP_ID }],
58+
});
59+
expect(TestBed.inject(LOCAL_STORAGE_TOKEN)).toBeUndefined();
60+
});
61+
it('should contain undefined on platform worker ui', () => {
62+
TestBed.configureTestingModule({
63+
providers: [{ provide: PLATFORM_ID, useValue: PLATFORM_WORKER_UI_ID }],
64+
});
65+
expect(TestBed.inject(LOCAL_STORAGE_TOKEN)).toBeUndefined();
66+
});
67+
});
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: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { isPlatformBrowser } from '@angular/common';
2+
import { inject, InjectionToken, PLATFORM_ID, Provider } from '@angular/core';
3+
4+
/**
5+
* Injection Token to safely inject
6+
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage} to Angular DI
7+
*/
8+
export const SESSION_STORAGE_TOKEN: InjectionToken<Storage | undefined> = new InjectionToken<
9+
Storage | undefined
10+
>('SESSION_STORAGE_TOKEN', {
11+
providedIn: 'root',
12+
factory: () => (isPlatformBrowser(inject(PLATFORM_ID)) ? sessionStorage : undefined),
13+
});
14+
15+
/**
16+
* Injection Token to safely inject
17+
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage} to Angular DI
18+
*/
19+
export const LOCAL_STORAGE_TOKEN: InjectionToken<Storage | undefined> = new InjectionToken<
20+
Storage | undefined
21+
>('LOCAL_STORAGE_TOKEN', {
22+
providedIn: 'root',
23+
factory: () => (isPlatformBrowser(inject(PLATFORM_ID)) ? localStorage : undefined),
24+
});
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)