Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ your forms elements. It builds on existing Angular functionality like
and
[NgControl](https://angular.io/docs/ts/latest/api/forms/index/NgControl-class.html)

This supports both [Tempalte driven forms](https://angular.io/guide/forms) and [Reactive driven forms](https://angular.io/guide/reactive-forms).

#### Template Driven

For the simplest use-cases, the API is very straightforward. Your template
would look something like this:

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

#### Reactive Forms
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.

```html
<form connect="myForm" [formGroup]="loginForm">
<input type="text" name="address" formControlName="firstName" />
</form>
```

#### Troubleshooting

If you are having trouble getting data-binding to work for an element of your form,
Expand Down
4 changes: 2 additions & 2 deletions source/connect-array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import {Unsubscribe} from 'redux';

import {Subscription} from 'rxjs';

import {Connect} from './connect';
import {ConnectBase} from './connect-base';
import {FormStore} from './form-store';
import {State} from './state';
import {controlPath, selectValueAccessor} from './shims';
Expand Down Expand Up @@ -75,7 +75,7 @@ export class ConnectArray extends ControlContainer implements OnInit {
@Optional() @Self() @Inject(NG_VALIDATORS) private rawValidators: any[],
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private rawAsyncValidators: any[],
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: any[],
private connection: Connect,
private connection: ConnectBase,
private templateRef: TemplateRef<any>,
private viewContainerRef: ViewContainerRef,
private store: FormStore,
Expand Down
139 changes: 139 additions & 0 deletions source/connect-base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import {
Directive,
Input,
} from '@angular/core';

import {
AbstractControl,
FormControl,
FormGroup,
FormArray,
NgForm,
NgControl,
} from '@angular/forms';

import { Subscription } from 'rxjs';

import { Unsubscribe } from 'redux';

import 'rxjs/add/operator/debounceTime';

import { FormException } from './form-exception';
import { FormStore } from './form-store';
import { State } from './state';

export interface ControlPair {
path: Array<string>;
control: AbstractControl;
}

export class ConnectBase {

@Input('connect') connect: () => (string | number) | Array<string | number>;
private stateSubscription: Unsubscribe;

private formSubscription: Subscription;
protected store: FormStore;
protected form;

public get path(): Array<string> {
const path = typeof this.connect === 'function'
? this.connect()
: this.connect;

switch (typeof path) {
case 'object':
if (State.empty(path)) {
return [];
}
if (Array.isArray(path)) {
return <Array<string>>path;
}
case 'string':
return (<string>path).split(/\./g);
default: // fallthrough above (no break)
throw new Error(`Cannot determine path to object: ${JSON.stringify(path)}`);
}
}

ngOnDestroy() {
if (this.formSubscription) {
this.formSubscription.unsubscribe();
}

if (typeof this.stateSubscription === 'function') {
this.stateSubscription(); // unsubscribe
}
}

private ngAfterContentInit() {
Promise.resolve().then(() => {
this.resetState();

this.stateSubscription = this.store.subscribe(state => {
this.resetState();
});

Promise.resolve().then(() => {
this.formSubscription = (<any>this.form.valueChanges).debounceTime(0).subscribe(values => this.publish(values));
});
});
}

private descendants(path: Array<string>, formElement): Array<ControlPair> {
const pairs = new Array<ControlPair>();

if (formElement instanceof FormArray) {
formElement.controls.forEach((c, index) => {
for (const d of this.descendants((<any>path).concat([index]), c)) {
pairs.push(d);
}
})
}
else if (formElement instanceof FormGroup) {
for (const k of Object.keys(formElement.controls)) {
pairs.push({ path: path.concat([k]), control: formElement.controls[k] });
}
}
else if (formElement instanceof NgControl || formElement instanceof FormControl) {
return [{ path: path, control: <any>formElement }];
}
else {
throw new Error(`Unknown type of form element: ${formElement.constructor.name}`);
}

return pairs.filter(p => (<any>p.control)._parent === this.form.control);
}

private resetState() {
var formElement;
if (this.form.control === undefined) {
formElement = this.form;
}
else {
formElement = this.form.control;
}

const children = this.descendants([], formElement);

children.forEach(c => {
const { path, control } = c;

const value = State.get(this.getState(), this.path.concat(c.path));

if (control.value !== value) {
const phonyControl = <any>{ path: path };

this.form.updateModel(phonyControl, value);
}
});
}

private publish(value) {
this.store.valueChanged(this.path, this.form, value);
}

private getState() {
return this.store.getState();
}
}
24 changes: 24 additions & 0 deletions source/connect-reactive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {
Directive,
Input,
} from '@angular/core';

import {
NgForm
} from '@angular/forms';

import {FormStore} from './form-store';

import {ConnectBase} from './connect-base';

// For reactive forms (without implicit NgForm)
@Directive({ selector: 'form[connect][formGroup]' })
export class ReactiveConnect extends ConnectBase {
@Input('formGroup') form;

constructor(
protected store: FormStore
) {
super();
}
}
129 changes: 9 additions & 120 deletions source/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,134 +4,23 @@ import {
} from '@angular/core';

import {
AbstractControl,
FormControl,
FormGroup,
FormArray,
NgForm,
NgControl,
NgForm
} from '@angular/forms';

import {Subscription} from 'rxjs';

import {Unsubscribe} from 'redux';

import 'rxjs/add/operator/debounceTime';

import {FormException} from './form-exception';
import {FormStore} from './form-store';
import {State} from './state';
import {ConnectBase} from './connect-base';

export interface ControlPair {
path: Array<string>;
control: AbstractControl;
}

@Directive({
selector: 'form[connect]',
})
export class Connect {
@Input('connect') connect: () => (string | number) | Array<string | number>;

private stateSubscription: Unsubscribe;

private formSubscription: Subscription;
// For template forms (with implicit NgForm)
@Directive({ selector: 'form[connect]:not([formGroup])' })
export class Connect extends ConnectBase {

constructor(
private store: FormStore,
private form: NgForm
) {}

public get path(): Array<string> {
const path = typeof this.connect === 'function'
? this.connect()
: this.connect;

switch (typeof path) {
case 'object':
if (State.empty(path)) {
return [];
}
if (Array.isArray(path)) {
return <Array<string>> path;
}
case 'string':
return (<string> path).split(/\./g);
default: // fallthrough above (no break)
throw new Error(`Cannot determine path to object: ${JSON.stringify(path)}`);
}
}

ngOnDestroy () {
if (this.formSubscription) {
this.formSubscription.unsubscribe();
}

if (typeof this.stateSubscription === 'function') {
this.stateSubscription(); // unsubscribe
}
}

private ngAfterContentInit() {
Promise.resolve().then(() => {
this.resetState();

this.stateSubscription = this.store.subscribe(state => {
this.resetState();
});

Promise.resolve().then(() => {
this.formSubscription = (<any>this.form.valueChanges).debounceTime(0).subscribe(values => this.publish(values));
});
});
}

private descendants(path: Array<string>, formElement): Array<ControlPair> {
const pairs = new Array<ControlPair>();

if (formElement instanceof FormArray) {
formElement.controls.forEach((c, index) => {
for (const d of this.descendants((<any>path).concat([index]), c)) {
pairs.push(d);
}
})
}
else if (formElement instanceof FormGroup) {
for (const k of Object.keys(formElement.controls)) {
pairs.push({path:path.concat([k]), control: formElement.controls[k]});
}
}
else if (formElement instanceof NgControl || formElement instanceof FormControl) {
return [{path: path, control: <any> formElement}];
}
else {
throw new Error(`Unknown type of form element: ${formElement.constructor.name}`);
}

return pairs.filter(p => (<any>p.control)._parent === this.form.control);
}

private resetState() {
const children = this.descendants([], this.form.control);

children.forEach(c => {
const {path, control} = c;

const value = State.get(this.getState(), this.path.concat(c.path));

if (control.value !== value) {
const phonyControl = <any>{path: path};

this.form.updateModel(phonyControl, value);
}
});
}

private publish(value) {
this.store.valueChanged(this.path, this.form, value);
}

private getState() {
return this.store.getState();
protected store: FormStore,
protected form: NgForm
) {
super();
}
}
2 changes: 2 additions & 0 deletions source/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export * from './form-reducer';
export * from './form-exception';
export * from './form-store';
export * from './configure';
export * from './connect-base';
export * from './connect-reactive';
export * from './connect';
export * from './connect-array';
export * from './module';
3 changes: 3 additions & 0 deletions source/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {FormsModule, ReactiveFormsModule} from '@angular/forms';

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

import {ReactiveConnect} from './connect-reactive';
import {Connect} from './connect';
import {ConnectArray} from './connect-array';
import {FormStore} from './form-store';
Expand All @@ -18,10 +19,12 @@ export function formStoreFactory(ngRedux: NgRedux<any>) {
],
declarations: [
Connect,
ReactiveConnect,
ConnectArray,
],
exports: [
Connect,
ReactiveConnect,
ConnectArray,
],
providers: [
Expand Down