Skip to content

Fix reactive forms, debounce inputs #48

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
107 changes: 55 additions & 52 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ Both `NgRedux<T>` and `Redux.Store<T>` 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<RootState>`.

#### Input debounce

To debounce emitted FORM_CHANGED actions simply specify the desired debounce time in milliseconds on the form:

```html
<form connect="myForm" debounce="500">
```

### How the bindings work

The bindings work by inspecting the shape of your form and then binding to a Redux
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
46 changes: 34 additions & 12 deletions source/connect/connect-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,15 @@ export interface ControlPair {
export class ConnectBase {

@Input('connect') connect: () => (string | number) | Array<string | number>;
@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<string> {
const path = typeof this.connect === 'function'
Expand Down Expand Up @@ -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 = (<any>this.form.valueChanges)
.debounceTime(0)
.debounceTime(this.changeDebounce)
.subscribe((values: any) => this.publish(values));
});
});
Expand All @@ -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) {
Expand All @@ -97,11 +109,14 @@ export class ConnectBase {
throw new Error(`Unknown type of form element: ${formElement.constructor.name}`);
}

return pairs.filter(p => (<any>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;
}
Expand All @@ -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 = <any>{ 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});
}
});
}
Expand Down
6 changes: 5 additions & 1 deletion source/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,13 @@ export abstract class State {
else if (deepValue instanceof Map) {
deepValue = (<Map<string, any>> <any> 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);
Expand Down