diff --git a/README.md b/README.md index f087775..7f7a256 100644 --- a/README.md +++ b/README.md @@ -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: @@ -203,6 +207,15 @@ the `path` property on our first ` + +``` + #### Troubleshooting If you are having trouble getting data-binding to work for an element of your form, diff --git a/source/connect-array.ts b/source/connect-array.ts index de91ef0..baca880 100644 --- a/source/connect-array.ts +++ b/source/connect-array.ts @@ -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'; @@ -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, private viewContainerRef: ViewContainerRef, private store: FormStore, diff --git a/source/connect-base.ts b/source/connect-base.ts new file mode 100644 index 0000000..27a201e --- /dev/null +++ b/source/connect-base.ts @@ -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; + control: AbstractControl; +} + +export class ConnectBase { + + @Input('connect') connect: () => (string | number) | Array; + private stateSubscription: Unsubscribe; + + private formSubscription: Subscription; + protected store: FormStore; + protected form; + + public get path(): Array { + 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 >path; + } + case 'string': + return (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 = (this.form.valueChanges).debounceTime(0).subscribe(values => this.publish(values)); + }); + }); + } + + private descendants(path: Array, formElement): Array { + const pairs = new Array(); + + if (formElement instanceof FormArray) { + formElement.controls.forEach((c, index) => { + for (const d of this.descendants((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: formElement }]; + } + else { + throw new Error(`Unknown type of form element: ${formElement.constructor.name}`); + } + + return pairs.filter(p => (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 = { path: path }; + + this.form.updateModel(phonyControl, value); + } + }); + } + + private publish(value) { + this.store.valueChanged(this.path, this.form, value); + } + + private getState() { + return this.store.getState(); + } +} diff --git a/source/connect-reactive.ts b/source/connect-reactive.ts new file mode 100644 index 0000000..3f361ac --- /dev/null +++ b/source/connect-reactive.ts @@ -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(); + } +} diff --git a/source/connect.ts b/source/connect.ts index e3618aa..ba43ea6 100644 --- a/source/connect.ts +++ b/source/connect.ts @@ -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; - control: AbstractControl; -} - -@Directive({ - selector: 'form[connect]', -}) -export class Connect { - @Input('connect') connect: () => (string | number) | Array; - 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 { - 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 > path; - } - case 'string': - return ( 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 = (this.form.valueChanges).debounceTime(0).subscribe(values => this.publish(values)); - }); - }); - } - - private descendants(path: Array, formElement): Array { - const pairs = new Array(); - - if (formElement instanceof FormArray) { - formElement.controls.forEach((c, index) => { - for (const d of this.descendants((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: formElement}]; - } - else { - throw new Error(`Unknown type of form element: ${formElement.constructor.name}`); - } - - return pairs.filter(p => (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 = {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(); } } diff --git a/source/index.ts b/source/index.ts index d942e12..de0191d 100644 --- a/source/index.ts +++ b/source/index.ts @@ -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'; diff --git a/source/module.ts b/source/module.ts index b24d255..eca3bb4 100644 --- a/source/module.ts +++ b/source/module.ts @@ -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'; @@ -18,10 +19,12 @@ export function formStoreFactory(ngRedux: NgRedux) { ], declarations: [ Connect, + ReactiveConnect, ConnectArray, ], exports: [ Connect, + ReactiveConnect, ConnectArray, ], providers: [