Skip to content
Open
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