Skip to content

Commit

Permalink
feat(storage): add support for async storage typescloses #24
Browse files Browse the repository at this point in the history
  • Loading branch information
tehshin committed Aug 2, 2020
1 parent eae2ba7 commit 6cd39d8
Show file tree
Hide file tree
Showing 9 changed files with 116 additions and 55 deletions.
8 changes: 4 additions & 4 deletions projects/ngneat/reactive-forms/src/lib/control-actions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ValidationErrors, FormArray as NgFormArray } from '@angular/forms';
import { defer, merge, Observable, of, Subscription } from 'rxjs';
import { distinctUntilChanged, map, tap, debounceTime } from 'rxjs/operators';
import { distinctUntilChanged, map, tap, debounceTime, switchMap } from 'rxjs/operators';
import { FormArray } from './formArray';
import { FormControl } from './formControl';
import { FormGroup } from './formGroup';
Expand All @@ -13,7 +13,7 @@ import {
PersistOptions,
ControlFactoryMap
} from './types';
import { coerceArray, isNil } from './utils';
import { coerceArray, isNil, wrapIntoObservable } from './utils';

function getControlValue<T>(control: AbstractControl<T>): T {
if ((control as any).getRawValue) {
Expand Down Expand Up @@ -130,7 +130,7 @@ export function hasErrorAndDirty<T>(control: AbstractControl<T>, error: string,

export function markAllDirty<T>(control: FormArray<T> | FormGroup<T>): void {
control.markAsDirty({ onlySelf: true });
(control as any)._forEachChild(control => control.markAllAsDirty?.());
(control as any)._forEachChild(control => control.markAllAsDirty());
}

export function selectControlValue$<T, R>(
Expand All @@ -143,7 +143,7 @@ export function selectControlValue$<T, R>(
export function persistValue$<T>(control: FormGroup<T>, key: string, options: PersistOptions<T>): Observable<T> {
return control.valueChanges.pipe(
debounceTime(options.debounceTime),
tap(value => options.manager.setValue(key, value))
switchMap(value => wrapIntoObservable(options.manager.setValue(key, value)))
);
}

Expand Down
96 changes: 65 additions & 31 deletions projects/ngneat/reactive-forms/src/lib/formGroup.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { fakeAsync, tick } from '@angular/core/testing';
import { of, Subject } from 'rxjs';
import { fakeAsync, tick, flush } from '@angular/core/testing';
import { of, Subject, Observable, timer, from } from 'rxjs';
import { FormControl } from './formControl';
import { FormGroup } from './formGroup';
import { FormArray } from './formArray';
import { switchMap } from 'rxjs/operators';
import { wrapIntoObservable } from './utils';

type Person = {
name: string;
Expand Down Expand Up @@ -296,34 +298,66 @@ describe('FormGroup', () => {
expect(spy).toHaveBeenCalledWith({ invalidName: true });
});

it('should persist', fakeAsync(() => {
const control = createGroup();
const debounceTime = 50;
const persistManager = {
getValue: jest.fn(),
setValue: jest.fn()
};
control.persist('key', { debounceTime, manager: persistManager }).subscribe();
control.getControl('name').setValue('ewan');
tick(debounceTime);
expect(persistManager.setValue).toHaveBeenCalledWith('key', control.value);
}));

it('should restore', () => {
const control = createGroup();
const person = { name: 'ewan', phone: { num: 5550153, prefix: 288 }, skills: ['acting', 'motorcycle'] };
const persistManager = {
getValue: jest.fn<Person, never>(() => person),
setValue: jest.fn()
};
const spy = jest.fn(value => new FormControl(value));
control.persist('key', {
manager: persistManager,
arrControlFactory: { skills: spy }
});
expect(persistManager.getValue).toHaveBeenCalledWith('key');
expect(spy).toHaveBeenCalledTimes(2);
expect(control.getControl('skills')).toHaveLength(2);
expect(control.value).toEqual(person);
describe('.persist()', () => {
const person: Person = { name: 'ewan', phone: { num: 5550153, prefix: 288 }, skills: ['acting', 'motorcycle'] };

it.each([[0], [300], [500]])(
'should persist',
fakeAsync((tickMs: number) => {
const control = createGroup();
const debounceTime = 50;
const persistManager = {
getValue: jest.fn(),
setValue: jest.fn((key, value) => {
return tickMs ? timer(tickMs).pipe(switchMap(() => of(value))) : value;
})
};
let persistValue: Person;
control.persist('key', { debounceTime, manager: persistManager }).subscribe(value => (persistValue = value));
control.getControl('name').setValue('ewan');
tick(debounceTime);
control.getControl('name').setValue('ewan mc');
tick(debounceTime);
expect(persistManager.setValue).toHaveBeenCalledTimes(2);
expect(persistManager.setValue).toHaveBeenLastCalledWith('key', control.value);
if (tickMs) {
expect(persistValue).toBeFalsy();
tick(tickMs);
expect(persistValue.name).toEqual('ewan mc');
}
})
);

it.each([
[person, 0],
[Promise.resolve(person), 300],
[of(person), 500]
])(
'should restore',
fakeAsync((value: Person | Promise<Person> | Observable<Person>, tickMs: number) => {
const control = createGroup();
const arrFactorySpy = jest.fn(value => new FormControl(value));
const persistManager = {
getValue: jest.fn<any, never>(() => {
return tickMs ? timer(tickMs).pipe(switchMap(() => wrapIntoObservable(value))) : value;
}),
setValue: jest.fn()
};
control
.persist('key', {
manager: persistManager,
arrControlFactory: { skills: arrFactorySpy }
})
.subscribe();
expect(persistManager.getValue).toHaveBeenCalledWith('key');
if (tickMs) {
expect(control.value).not.toEqual(person);
tick(tickMs);
expect(control.value).toEqual(person);
}
expect(arrFactorySpy).toHaveBeenCalledTimes(2);
expect(control.getControl('skills')).toHaveLength(2);
})
);
});
});
35 changes: 21 additions & 14 deletions projects/ngneat/reactive-forms/src/lib/formGroup.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FormGroup as NgFormGroup, FormArray as NgFormArray } from '@angular/forms';
import { isObservable, Observable, Subject, Subscription } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
import { distinctUntilChanged, tap, take, switchMap } from 'rxjs/operators';
import {
controlDisabled$,
controlDisabledWhile,
Expand Down Expand Up @@ -37,7 +37,7 @@ import {
PersistOptions,
ControlFactoryMap
} from './types';
import { coerceArray } from './utils';
import { coerceArray, wrapIntoObservable } from './utils';
import { PersistManager } from './persistManager';
import { LocalStorageManager } from './localStorageManager';
import { FormArray } from './formArray';
Expand Down Expand Up @@ -279,17 +279,24 @@ export class FormGroup<T extends Obj = any, E extends object = any> extends NgFo

persist(key: string, { debounceTime, manager, arrControlFactory }: PersistOptions<T>): Observable<T> {
const persistManager = manager || new LocalStorageManager();
this.restore(key, persistManager, arrControlFactory);
return persistValue$(this, key, {
debounceTime: debounceTime || 250,
manager: persistManager
});
}

private restore(key: string, manager: PersistManager<T>, arrControlFactory: ControlFactoryMap<T>) {
const value = manager.getValue(key);
if (!value) return;
handleFormArrays(this, value, arrControlFactory);
this.patchValue(value, { emitEvent: false });
return this.restore(key, persistManager, arrControlFactory).pipe(
switchMap(() =>
persistValue$(this, key, {
debounceTime: debounceTime || 250,
manager: persistManager
})
)
);
}

private restore(key: string, manager: PersistManager<T>, arrControlFactory: ControlFactoryMap<T>): Observable<T> {
return wrapIntoObservable<T>(manager.getValue(key)).pipe(
take(1),
tap(value => {
if (!value) return;
handleFormArrays(this, value, arrControlFactory);
this.patchValue(value, { emitEvent: false });
})
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ describe('LocalStorageManager', () => {
const person = { name: 'ewan', phone: { num: 5550153, prefix: 288 } };
const spy = jest.spyOn(localStorage, 'setItem').mockImplementation(() => {});
const storageManager = createStorageManager();
storageManager.setValue('key', person);
const response = storageManager.setValue('key', person);
expect(spy).toHaveBeenCalledWith('key', JSON.stringify(person));
expect(response).toEqual(person);
});

it('should get value', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { PersistManager } from './persistManager';

export class LocalStorageManager<T> implements PersistManager<T> {
setValue(key: string, data: T): void {
setValue(key: string, data: T): T {
localStorage.setItem(key, JSON.stringify(data));
return data;
}

getValue(key: string): T {
Expand Down
6 changes: 4 additions & 2 deletions projects/ngneat/reactive-forms/src/lib/persistManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Observable } from 'rxjs';

export interface PersistManager<T> {
setValue(key: string, data: T): void;
getValue(key: string): T;
setValue(key: string, data: T): T | Promise<T> | Observable<T>;
getValue(key: string): T | Promise<T> | Observable<T>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ describe('SessionStorageManager', () => {
const person = { name: 'ewan', phone: { num: 5550153, prefix: 288 } };
const spy = jest.spyOn(sessionStorage, 'setItem').mockImplementation(() => {});
const storageManager = createStorageManager();
storageManager.setValue('key', person);
const response = storageManager.setValue('key', person);
expect(spy).toHaveBeenCalledWith('key', JSON.stringify(person));
expect(response).toEqual(person);
});

it('should get value', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { PersistManager } from './persistManager';

export class SessionStorageManager<T> implements PersistManager<T> {
setValue(key: string, data: T): void {
setValue(key: string, data: T): T {
sessionStorage.setItem(key, JSON.stringify(data));
return data;
}

getValue(key: string): T {
Expand Down
14 changes: 14 additions & 0 deletions projects/ngneat/reactive-forms/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { from, isObservable, Observable, of } from 'rxjs';

export function coerceArray<T>(value: T | T[]): T[] {
return Array.isArray(value) ? value : [value];
}
Expand All @@ -9,3 +11,15 @@ export function isFunction(x: any): x is Function {
export function isNil(v: any): boolean {
return v === null || v === undefined;
}

export function isPromise(value: any): value is Promise<unknown> {
return typeof value?.then === 'function';
}

export function wrapIntoObservable<T>(value: T | Promise<T> | Observable<T>): Observable<T> {
if (isObservable(value) || isPromise(value)) {
return from(value);
}

return of(value);
}

0 comments on commit 6cd39d8

Please sign in to comment.