Skip to content

Commit 6a75993

Browse files
GenZoddSethDavenport
authored andcommitted
Update so it works with Reactive forms (#27)
* updated to work with reactive and template driven forms. * added a little documentation * updated based on PR feedback * updated based on PR feedback.
1 parent abfb518 commit 6a75993

File tree

7 files changed

+192
-122
lines changed

7 files changed

+192
-122
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ your forms elements. It builds on existing Angular functionality like
99
and
1010
[NgControl](https://angular.io/docs/ts/latest/api/forms/index/NgControl-class.html)
1111

12+
This supports both [Tempalte driven forms](https://angular.io/guide/forms) and [Reactive driven forms](https://angular.io/guide/reactive-forms).
13+
14+
#### Template Driven
15+
1216
For the simplest use-cases, the API is very straightforward. Your template
1317
would look something like this:
1418

@@ -203,6 +207,15 @@ the `path` property on our first `<select>` element, it would look like this:
203207
From there, `@angular-redux/form` is able to take that path and extract the value for
204208
that element from the Redux state.
205209

210+
#### Reactive Forms
211+
The value in "connect" attribute is the value that will show up in the Redux store. The formGroup value is the name of the object in your code that represents the form group.
212+
213+
```html
214+
<form connect="myForm" [formGroup]="loginForm">
215+
<input type="text" name="address" formControlName="firstName" />
216+
</form>
217+
```
218+
206219
#### Troubleshooting
207220

208221
If you are having trouble getting data-binding to work for an element of your form,

source/connect-array.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import {Unsubscribe} from 'redux';
3939

4040
import {Subscription} from 'rxjs';
4141

42-
import {Connect} from './connect';
42+
import {ConnectBase} from './connect-base';
4343
import {FormStore} from './form-store';
4444
import {State} from './state';
4545
import {controlPath, selectValueAccessor} from './shims';
@@ -75,7 +75,7 @@ export class ConnectArray extends ControlContainer implements OnInit {
7575
@Optional() @Self() @Inject(NG_VALIDATORS) private rawValidators: any[],
7676
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private rawAsyncValidators: any[],
7777
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: any[],
78-
private connection: Connect,
78+
private connection: ConnectBase,
7979
private templateRef: TemplateRef<any>,
8080
private viewContainerRef: ViewContainerRef,
8181
private store: FormStore,

source/connect-base.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import {
2+
Directive,
3+
Input,
4+
} from '@angular/core';
5+
6+
import {
7+
AbstractControl,
8+
FormControl,
9+
FormGroup,
10+
FormArray,
11+
NgForm,
12+
NgControl,
13+
} from '@angular/forms';
14+
15+
import { Subscription } from 'rxjs';
16+
17+
import { Unsubscribe } from 'redux';
18+
19+
import 'rxjs/add/operator/debounceTime';
20+
21+
import { FormException } from './form-exception';
22+
import { FormStore } from './form-store';
23+
import { State } from './state';
24+
25+
export interface ControlPair {
26+
path: Array<string>;
27+
control: AbstractControl;
28+
}
29+
30+
export class ConnectBase {
31+
32+
@Input('connect') connect: () => (string | number) | Array<string | number>;
33+
private stateSubscription: Unsubscribe;
34+
35+
private formSubscription: Subscription;
36+
protected store: FormStore;
37+
protected form;
38+
39+
public get path(): Array<string> {
40+
const path = typeof this.connect === 'function'
41+
? this.connect()
42+
: this.connect;
43+
44+
switch (typeof path) {
45+
case 'object':
46+
if (State.empty(path)) {
47+
return [];
48+
}
49+
if (Array.isArray(path)) {
50+
return <Array<string>>path;
51+
}
52+
case 'string':
53+
return (<string>path).split(/\./g);
54+
default: // fallthrough above (no break)
55+
throw new Error(`Cannot determine path to object: ${JSON.stringify(path)}`);
56+
}
57+
}
58+
59+
ngOnDestroy() {
60+
if (this.formSubscription) {
61+
this.formSubscription.unsubscribe();
62+
}
63+
64+
if (typeof this.stateSubscription === 'function') {
65+
this.stateSubscription(); // unsubscribe
66+
}
67+
}
68+
69+
private ngAfterContentInit() {
70+
Promise.resolve().then(() => {
71+
this.resetState();
72+
73+
this.stateSubscription = this.store.subscribe(state => {
74+
this.resetState();
75+
});
76+
77+
Promise.resolve().then(() => {
78+
this.formSubscription = (<any>this.form.valueChanges).debounceTime(0).subscribe(values => this.publish(values));
79+
});
80+
});
81+
}
82+
83+
private descendants(path: Array<string>, formElement): Array<ControlPair> {
84+
const pairs = new Array<ControlPair>();
85+
86+
if (formElement instanceof FormArray) {
87+
formElement.controls.forEach((c, index) => {
88+
for (const d of this.descendants((<any>path).concat([index]), c)) {
89+
pairs.push(d);
90+
}
91+
})
92+
}
93+
else if (formElement instanceof FormGroup) {
94+
for (const k of Object.keys(formElement.controls)) {
95+
pairs.push({ path: path.concat([k]), control: formElement.controls[k] });
96+
}
97+
}
98+
else if (formElement instanceof NgControl || formElement instanceof FormControl) {
99+
return [{ path: path, control: <any>formElement }];
100+
}
101+
else {
102+
throw new Error(`Unknown type of form element: ${formElement.constructor.name}`);
103+
}
104+
105+
return pairs.filter(p => (<any>p.control)._parent === this.form.control);
106+
}
107+
108+
private resetState() {
109+
var formElement;
110+
if (this.form.control === undefined) {
111+
formElement = this.form;
112+
}
113+
else {
114+
formElement = this.form.control;
115+
}
116+
117+
const children = this.descendants([], formElement);
118+
119+
children.forEach(c => {
120+
const { path, control } = c;
121+
122+
const value = State.get(this.getState(), this.path.concat(c.path));
123+
124+
if (control.value !== value) {
125+
const phonyControl = <any>{ path: path };
126+
127+
this.form.updateModel(phonyControl, value);
128+
}
129+
});
130+
}
131+
132+
private publish(value) {
133+
this.store.valueChanged(this.path, this.form, value);
134+
}
135+
136+
private getState() {
137+
return this.store.getState();
138+
}
139+
}

source/connect-reactive.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import {
2+
Directive,
3+
Input,
4+
} from '@angular/core';
5+
6+
import {
7+
NgForm
8+
} from '@angular/forms';
9+
10+
import {FormStore} from './form-store';
11+
12+
import {ConnectBase} from './connect-base';
13+
14+
// For reactive forms (without implicit NgForm)
15+
@Directive({ selector: 'form[connect][formGroup]' })
16+
export class ReactiveConnect extends ConnectBase {
17+
@Input('formGroup') form;
18+
19+
constructor(
20+
protected store: FormStore
21+
) {
22+
super();
23+
}
24+
}

source/connect.ts

Lines changed: 9 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -4,134 +4,23 @@ import {
44
} from '@angular/core';
55

66
import {
7-
AbstractControl,
8-
FormControl,
9-
FormGroup,
10-
FormArray,
11-
NgForm,
12-
NgControl,
7+
NgForm
138
} from '@angular/forms';
149

15-
import {Subscription} from 'rxjs';
1610

17-
import {Unsubscribe} from 'redux';
18-
19-
import 'rxjs/add/operator/debounceTime';
20-
21-
import {FormException} from './form-exception';
2211
import {FormStore} from './form-store';
2312
import {State} from './state';
13+
import {ConnectBase} from './connect-base';
2414

25-
export interface ControlPair {
26-
path: Array<string>;
27-
control: AbstractControl;
28-
}
29-
30-
@Directive({
31-
selector: 'form[connect]',
32-
})
33-
export class Connect {
34-
@Input('connect') connect: () => (string | number) | Array<string | number>;
3515

36-
private stateSubscription: Unsubscribe;
37-
38-
private formSubscription: Subscription;
16+
// For template forms (with implicit NgForm)
17+
@Directive({ selector: 'form[connect]:not([formGroup])' })
18+
export class Connect extends ConnectBase {
3919

4020
constructor(
41-
private store: FormStore,
42-
private form: NgForm
43-
) {}
44-
45-
public get path(): Array<string> {
46-
const path = typeof this.connect === 'function'
47-
? this.connect()
48-
: this.connect;
49-
50-
switch (typeof path) {
51-
case 'object':
52-
if (State.empty(path)) {
53-
return [];
54-
}
55-
if (Array.isArray(path)) {
56-
return <Array<string>> path;
57-
}
58-
case 'string':
59-
return (<string> path).split(/\./g);
60-
default: // fallthrough above (no break)
61-
throw new Error(`Cannot determine path to object: ${JSON.stringify(path)}`);
62-
}
63-
}
64-
65-
ngOnDestroy () {
66-
if (this.formSubscription) {
67-
this.formSubscription.unsubscribe();
68-
}
69-
70-
if (typeof this.stateSubscription === 'function') {
71-
this.stateSubscription(); // unsubscribe
72-
}
73-
}
74-
75-
private ngAfterContentInit() {
76-
Promise.resolve().then(() => {
77-
this.resetState();
78-
79-
this.stateSubscription = this.store.subscribe(state => {
80-
this.resetState();
81-
});
82-
83-
Promise.resolve().then(() => {
84-
this.formSubscription = (<any>this.form.valueChanges).debounceTime(0).subscribe(values => this.publish(values));
85-
});
86-
});
87-
}
88-
89-
private descendants(path: Array<string>, formElement): Array<ControlPair> {
90-
const pairs = new Array<ControlPair>();
91-
92-
if (formElement instanceof FormArray) {
93-
formElement.controls.forEach((c, index) => {
94-
for (const d of this.descendants((<any>path).concat([index]), c)) {
95-
pairs.push(d);
96-
}
97-
})
98-
}
99-
else if (formElement instanceof FormGroup) {
100-
for (const k of Object.keys(formElement.controls)) {
101-
pairs.push({path:path.concat([k]), control: formElement.controls[k]});
102-
}
103-
}
104-
else if (formElement instanceof NgControl || formElement instanceof FormControl) {
105-
return [{path: path, control: <any> formElement}];
106-
}
107-
else {
108-
throw new Error(`Unknown type of form element: ${formElement.constructor.name}`);
109-
}
110-
111-
return pairs.filter(p => (<any>p.control)._parent === this.form.control);
112-
}
113-
114-
private resetState() {
115-
const children = this.descendants([], this.form.control);
116-
117-
children.forEach(c => {
118-
const {path, control} = c;
119-
120-
const value = State.get(this.getState(), this.path.concat(c.path));
121-
122-
if (control.value !== value) {
123-
const phonyControl = <any>{path: path};
124-
125-
this.form.updateModel(phonyControl, value);
126-
}
127-
});
128-
}
129-
130-
private publish(value) {
131-
this.store.valueChanged(this.path, this.form, value);
132-
}
133-
134-
private getState() {
135-
return this.store.getState();
21+
protected store: FormStore,
22+
protected form: NgForm
23+
) {
24+
super();
13625
}
13726
}

source/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ export * from './form-reducer';
33
export * from './form-exception';
44
export * from './form-store';
55
export * from './configure';
6+
export * from './connect-base';
7+
export * from './connect-reactive';
68
export * from './connect';
79
export * from './connect-array';
810
export * from './module';

source/module.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {FormsModule, ReactiveFormsModule} from '@angular/forms';
33

44
import {NgRedux} from '@angular-redux/store';
55

6+
import {ReactiveConnect} from './connect-reactive';
67
import {Connect} from './connect';
78
import {ConnectArray} from './connect-array';
89
import {FormStore} from './form-store';
@@ -18,10 +19,12 @@ export function formStoreFactory(ngRedux: NgRedux<any>) {
1819
],
1920
declarations: [
2021
Connect,
22+
ReactiveConnect,
2123
ConnectArray,
2224
],
2325
exports: [
2426
Connect,
27+
ReactiveConnect,
2528
ConnectArray,
2629
],
2730
providers: [

0 commit comments

Comments
 (0)