Skip to content

Commit 01ce2e5

Browse files
committed
fix(storage): should merge the object
1 parent 6c82909 commit 01ce2e5

File tree

6 files changed

+144
-37
lines changed

6 files changed

+144
-37
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ const isNameDisabled$ = formsManager.selectDisabled('onboarding', 'name');
108108

109109
```ts
110110
const value$ = formsManager.selectValue('onboarding');
111-
const nameValue$ = formsManager.selectValue('onboarding', 'name');
111+
const nameValue$ = formsManager.selectValue<string>('onboarding', 'name');
112112
```
113113

114114
- `selectErrors()` - Observe the control's errors

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

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Inject, Injectable, Optional } from '@angular/core';
22
import { AbstractControl, FormArray, FormControl, FormGroup } from '@angular/forms';
3-
import { clone, coerceArray, filterControlKeys, filterNil, isBrowser } from './utils';
3+
import { clone, coerceArray, filterControlKeys, filterNil, isBrowser, mergeDeep } from './utils';
44
import { merge, Observable, Subscription } from 'rxjs';
55
import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators';
66
import { FormsStore } from './forms-manager.store';
@@ -30,7 +30,9 @@ export class NgFormsManager<FormsState = any> {
3030
return this.selectControl(formName, path).pipe(map(control => control.disabled));
3131
}
3232

33-
selectValue<T extends keyof FormsState>(formName: T, path?: string): Observable<FormsState[T]> {
33+
selectValue<T extends keyof FormsState>(formName: T, path?: string): Observable<FormsState[T]>;
34+
selectValue<T = any>(formName: keyof FormsState, path: string): Observable<T>;
35+
selectValue(formName: keyof FormsState, path?: string): Observable<any> {
3436
return this.selectControl(formName, path).pipe(map(control => control.value));
3537
}
3638

@@ -41,9 +43,12 @@ export class NgFormsManager<FormsState = any> {
4143
/**
4244
* If no path specified it means that it's a single FormControl or FormArray
4345
*/
44-
selectControl(formName: keyof FormsState, path?: string): Observable<_AbstractControl> {
46+
selectControl<T = any>(
47+
formName: keyof FormsState,
48+
path?: string
49+
): Observable<_AbstractControl<T>> {
4550
if (!path) {
46-
return this.selectForm(formName);
51+
return this.selectForm(formName) as any;
4752
}
4853

4954
return this.store
@@ -55,10 +60,9 @@ export class NgFormsManager<FormsState = any> {
5560
);
5661
}
5762

58-
// TODO: _AbstractControl should take a generic that should type the `value` property
59-
getControl(formName: keyof FormsState, path?: string): _AbstractControl {
63+
getControl<T = any>(formName: keyof FormsState, path?: string): _AbstractControl<T> {
6064
if (!path) {
61-
return this.getForm(formName);
65+
return this.getForm(formName) as any;
6266
}
6367

6468
if (this.hasForm(formName)) {
@@ -69,11 +73,10 @@ export class NgFormsManager<FormsState = any> {
6973
return null;
7074
}
7175

72-
// TODO: _AbstractControl should take a generic that should type the `value` property
73-
selectForm(
74-
formName: keyof FormsState,
76+
selectForm<T extends keyof FormsState>(
77+
formName: T,
7578
options: { filterNil: true } = { filterNil: true }
76-
): Observable<AbstractGroup> {
79+
): Observable<AbstractGroup<FormsState[T]>> {
7780
return this.store
7881
.select(state => state[formName as any])
7982
.pipe(options.filterNil ? filterNil : s => s);
@@ -98,31 +101,31 @@ export class NgFormsManager<FormsState = any> {
98101
arrControlFactory?: ControlFactory | HashMap<ControlFactory>;
99102
} = {}
100103
) {
101-
const merged = this.config.merge(config) as Config & { arrControlFactory; persistState };
104+
const mergedConfig = this.config.merge(config) as Config & { arrControlFactory; persistState };
102105
if (isBrowser() && config.persistState && this.hasForm(formName) === false) {
103-
const storageValue = this.getFromStorage(merged.storage.key);
106+
const storageValue = this.getFromStorage(mergedConfig.storage.key);
104107
if (storageValue[formName]) {
105108
this.store.update({
106-
[formName]: storageValue[formName],
109+
[formName]: mergeDeep(this.buildFormStoreState(formName, form), storageValue[formName]),
107110
} as Partial<FormsState>);
108111
}
109112
}
110113

111114
/** If the form already exist, patch the form with the store value */
112115
if (this.hasForm(formName) === true) {
113-
form.patchValue(this.resolveStoreToForm(formName, form, merged.arrControlFactory), {
116+
form.patchValue(this.resolveStoreToForm(formName, form, mergedConfig.arrControlFactory), {
114117
emitEvent: false,
115118
});
116119
} else {
117120
const value = this.updateStore(formName, form);
118-
this.updateStorage(formName, value, merged);
121+
this.updateStorage(formName, value, mergedConfig);
119122
}
120123

121124
const unsubscribe = merge(form.valueChanges, form.statusChanges.pipe(distinctUntilChanged()))
122-
.pipe(debounceTime(merged.debounceTime))
125+
.pipe(debounceTime(mergedConfig.debounceTime))
123126
.subscribe(() => {
124127
const value = this.updateStore(formName, form);
125-
this.updateStorage(formName, value, merged);
128+
this.updateStorage(formName, value, mergedConfig);
126129
});
127130

128131
this.valueChanges.set(formName, unsubscribe);
@@ -131,13 +134,27 @@ export class NgFormsManager<FormsState = any> {
131134
return this;
132135
}
133136

134-
patchValue(formName: keyof FormsState, value: any, options) {
137+
patchValue<T extends keyof FormsState>(
138+
formName: T,
139+
value: Partial<FormsState[T]>,
140+
options?: {
141+
onlySelf?: boolean;
142+
emitEvent?: boolean;
143+
}
144+
) {
135145
if (this.instances.has(formName)) {
136146
this.instances.get(formName).patchValue(value, options);
137147
}
138148
}
139149

140-
setValue(formName: keyof FormsState, value: any, options) {
150+
setValue<T extends keyof FormsState>(
151+
formName: keyof FormsState,
152+
value: FormsState[T],
153+
options?: {
154+
onlySelf?: boolean;
155+
emitEvent?: boolean;
156+
}
157+
) {
141158
if (this.instances.has(formName)) {
142159
this.instances.get(formName).setValue(value, options);
143160
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { NgFormsManager, NgFormsManagerConfig } from '@ngneat/forms-manager';
2+
import { FormControl, FormGroup } from '@angular/forms';
3+
4+
interface AppForms {
5+
formOne: string;
6+
formTwo: {
7+
name: string;
8+
age: number;
9+
};
10+
}
11+
12+
const manager = new NgFormsManager<AppForms>(new NgFormsManagerConfig({}));
13+
14+
const formOne = new FormControl('');
15+
16+
const formTwo = new FormGroup({
17+
name: new FormControl(''),
18+
age: new FormControl(),
19+
});
20+
21+
manager.upsert('formOne', formOne);
22+
manager.upsert('formTwo', formTwo);
23+
24+
manager.selectValid('formOne').subscribe(isValid => {
25+
// infer boolean
26+
});
27+
manager.selectDirty('formOne').subscribe(isDirty => {
28+
// infer boolean
29+
});
30+
manager.selectDisabled('formOne').subscribe(isDisabled => {
31+
// infer boolean
32+
});
33+
manager.selectValue('formOne').subscribe(value => {
34+
// infer string
35+
value.toLowerCase();
36+
});
37+
38+
manager.selectValue('formTwo').subscribe(group => {
39+
// infer { name: string, age: number }
40+
group.name;
41+
group.age;
42+
});
43+
44+
manager.selectValue<string>('formTwo', 'name').subscribe(name => {
45+
name.toLowerCase();
46+
});
47+
48+
manager.selectControl('formOne').subscribe(control => {
49+
// infer to string
50+
control.value.toLowerCase();
51+
});
52+
53+
manager.selectControl<AppForms['formTwo']>('formTwo').subscribe(group => {
54+
// infer { name: string, age: number }
55+
group.value.age;
56+
group.value.name;
57+
});
58+
59+
manager.selectControl<string>('formTwo', 'name').subscribe(nameControl => {
60+
console.log(nameControl.value);
61+
});
62+
63+
manager.selectForm('formOne').subscribe(control => {
64+
// infer to string
65+
control.value.toLowerCase();
66+
});
67+
68+
manager.selectForm('formTwo').subscribe(form => {
69+
// infer { name: string, age: number }
70+
form.value.name;
71+
form.value.age;
72+
});
73+
74+
manager.patchValue('formOne', '');
75+
manager.patchValue('formTwo', { name: '' });
76+
77+
// forces pass the whole object as the method expected
78+
manager.setValue('formTwo', { name: '', age: 3 });

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

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,11 @@
11
import { AbstractControl } from '@angular/forms';
22

3-
export type _AbstractControl = Pick<
3+
export type _AbstractControl<T = any> = Pick<
44
AbstractControl,
5-
| 'value'
6-
| 'valid'
7-
| 'invalid'
8-
| 'disabled'
9-
| 'errors'
10-
| 'touched'
11-
| 'pristine'
12-
| 'pending'
13-
| 'dirty'
14-
> & { rawValue: any };
5+
'valid' | 'invalid' | 'disabled' | 'errors' | 'touched' | 'pristine' | 'pending' | 'dirty'
6+
> & { rawValue: T; value: T };
157

16-
export interface AbstractGroup<C = any> extends _AbstractControl {
8+
export interface AbstractGroup<C = any> extends _AbstractControl<C> {
179
controls: { readonly [P in keyof C]: _AbstractControl };
1810
}
1911

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,25 @@ export function filterKeys(obj, cb) {
6969
export function isBrowser() {
7070
return typeof window !== 'undefined';
7171
}
72+
73+
export function mergeDeep(target, ...sources) {
74+
if (!sources.length) {
75+
return target;
76+
}
77+
const source = sources.shift();
78+
79+
if (isObject(target) && isObject(source)) {
80+
for (const key in source) {
81+
if (isObject(source[key])) {
82+
if (!target[key]) {
83+
Object.assign(target, { [key]: {} });
84+
}
85+
mergeDeep(target[key], source[key]);
86+
} else {
87+
Object.assign(target, { [key]: source[key] });
88+
}
89+
}
90+
}
91+
92+
return mergeDeep(target, ...sources);
93+
}

src/app/demo/demo.component.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,10 @@ export class DemoComponent implements OnInit {
4646
}),
4747
});
4848

49-
this.sub = this.formsManager.selectValue<number>('settings', 'minPrice').subscribe(minPrice => {
49+
this.sub = this.formsManager.selectValue('settings', 'minPrice').subscribe(minPrice => {
5050
setValidators(this.group.get('price'), Validators.min(minPrice));
5151
});
5252

53-
const createSkillControl = val => new FormControl(null, Validators.required);
54-
5553
this.settings = new FormGroup({
5654
minPrice: new FormControl(10),
5755
});
@@ -62,7 +60,7 @@ export class DemoComponent implements OnInit {
6260
.upsert('single', this.email, { persistState: true })
6361
.upsert('config', this.config, {
6462
persistState: true,
65-
arrControlFactory: { skills: createSkillControl },
63+
arrControlFactory: { skills: () => new FormControl(null, Validators.required) },
6664
})
6765
.upsert('group', this.group, {
6866
persistState: true,

0 commit comments

Comments
 (0)