diff --git a/.gitignore b/.gitignore index 9e50955..1531846 100644 --- a/.gitignore +++ b/.gitignore @@ -1,52 +1,55 @@ -# Logs -logs -*.log -**/npm-debug.log* - -.idea - -# Runtime data -pids -*.pid -*.seed - -# Temporary editor files -*.swp -*~ - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules -jspm_packages - -# Optional npm cache directory -.npm - -# Optional REPL history -.node_repl_history - -# Build output -.awcache -dist - -source/*ngfactory.ts -source/*ngsummary.json - -*.tgz +# Logs +logs +*.log +**/npm-debug.log* + +.idea +.settings + +# Runtime data +pids +*.pid +*.seed + +# Temporary editor files +*.swp +*~ + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Build output +.awcache +dist + +source/*ngfactory.ts +source/*ngsummary.json + +*.tgz +/.project +/.tern-project diff --git a/CHANGELOG.md b/CHANGELOG.md index a3c7f87..b806d8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 6.7.0 - Fix reactive forms, support custom debounce time for form inputs + +* https://github.com/angular-redux/form/pull/48 + # 6.5.1 - Support typescript unused checks * https://github.com/angular-redux/form/pull/32 diff --git a/README.md b/README.md index 7e3ba61..8d721dd 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,14 @@ Both `NgRedux` and `Redux.Store` conform to this shape. If you have a more complicated use-case that is not covered here, you could even create your own store shim as long as it conforms to the shape of `AbstractStore`. +#### Input debounce + +To debounce emitted FORM_CHANGED actions simply specify the desired debounce time in milliseconds on the form: + +```html +
+``` + ### How the bindings work The bindings work by inspecting the shape of your form and then binding to a Redux diff --git a/package.json b/package.json index bf1ba61..d52f4c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular-redux/form", - "version": "6.6.0", + "version": "6.7.0", "description": "Build Angular 2+ forms with Redux", "dependencies": { "immutable": "^3.8.1" diff --git a/source/connect/connect-base.ts b/source/connect/connect-base.ts index c4c1ecc..0c90b4e 100644 --- a/source/connect/connect-base.ts +++ b/source/connect/connect-base.ts @@ -25,11 +25,15 @@ export interface ControlPair { export class ConnectBase { @Input('connect') connect: () => (string | number) | Array; + @Input('debounce') debounce: number; private stateSubscription: Unsubscribe; private formSubscription: Subscription; protected store: FormStore; protected form: any; + protected get changeDebounce(): number { + return 'number' === typeof this.debounce || ('string' === typeof this.debounce && String(this.debounce).match(/^[0-9]+(\.[0-9]+)?$/)) ? Number(this.debounce) : 0; + } public get path(): Array { const path = typeof this.connect === 'function' @@ -63,13 +67,15 @@ export class ConnectBase { ngAfterContentInit() { Promise.resolve().then(() => { - this.resetState(); + // This is the first "change" of the form (setting initial values from the store) and thus should not emit a "changed" event + this.resetState(false); - this.stateSubscription = this.store.subscribe(() => this.resetState()); + // Any further changes on the state are due to application flow (e.g. user interaction triggering state changes) and thus have to trigger "changed" events + this.stateSubscription = this.store.subscribe(() => this.resetState(true)); Promise.resolve().then(() => { this.formSubscription = (this.form.valueChanges) - .debounceTime(0) + .debounceTime(this.changeDebounce) .subscribe((values: any) => this.publish(values)); }); }); @@ -87,7 +93,13 @@ export class ConnectBase { } else if (formElement instanceof FormGroup) { for (const k of Object.keys(formElement.controls)) { - pairs.push({ path: path.concat([k]), control: formElement.controls[k] }); + // If the control is a FormGroup or FormArray get the descendants of the the control instead of the control itself to always patch fields, not groups/arrays + if(formElement.controls[k] instanceof FormArray || formElement.controls[k] instanceof FormGroup) { + pairs.push(...this.descendants(path.concat([k]), formElement.controls[k])); + } + else { + pairs.push({ path: path.concat([k]), control: formElement.controls[k] }); + } } } else if (formElement instanceof NgControl || formElement instanceof FormControl) { @@ -97,11 +109,14 @@ export class ConnectBase { throw new Error(`Unknown type of form element: ${formElement.constructor.name}`); } - return pairs.filter(p => (p.control)._parent === this.form.control); + return pairs; } - private resetState() { + private resetState(emitEvent: boolean = true) { + emitEvent = !!emitEvent ? true : false; + var formElement; + if (this.form.control === undefined) { formElement = this.form; } @@ -114,12 +129,19 @@ export class ConnectBase { 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); + const value = State.get(this.getState(), this.path.concat(path)); + const newValueIsEmpty: boolean = 'undefined' === typeof value || null === value || ('string' === typeof value && '' === value); + const oldValueIsEmpty: boolean = 'undefined' === typeof control.value || null === control.value || ('string' === typeof control.value && '' === control.value); + + // patchValue() should only be called upon "real changes", meaning "null" and "undefined" should be treated equal to "" (empty string) + // newValueIsEmpty: true, oldValueIsEmpty: true => no change + // newValueIsEmpty: true, oldValueIsEmpty: false => change + // newValueIsEmpty: false, oldValueIsEmpty: true => change + // newValueIsEmpty: false, oldValueIsEmpty: false => + // control.value === value => no change + // control.value !== value => change + if (oldValueIsEmpty !== newValueIsEmpty || (!oldValueIsEmpty && !newValueIsEmpty && control.value !== value)) { + control.patchValue(newValueIsEmpty ? '' : value, {emitEvent}); } }); } diff --git a/source/state.ts b/source/state.ts index 671d288..5a9a71c 100644 --- a/source/state.ts +++ b/source/state.ts @@ -36,9 +36,13 @@ export abstract class State { else if (deepValue instanceof Map) { deepValue = (> deepValue).get(k); } - else { + else if('object' === typeof deepValue && !Array.isArray(deepValue) && null !== deepValue) { deepValue = (deepValue as any)[k]; } + else { + return undefined; + } + if (typeof fn === 'function') { const transformed = fn(parent, k, path.slice(path.indexOf(k) + 1), deepValue);