Say I have the following Angular 2 component, where foo
is an object not bound to a form, and not coming from an Input()
:
@Component({
selector: 'foo',
templateUrl: '/angular/views/foo.template.html',
directives: [ContentEditableDirective, ROUTER_DIRECTIVES],
providers: [FooService]
})
export class FooComponent implements OnInit {
public foo: Foo = new Foo();
constructor(
private fooService: FooService,
private route : ActivatedRoute,
private router : Router) {}
ngOnInit() {
let id = +this.route.snapshot.params['id'];
this.fooService.getFoo(id).subscribe(
foo => {
this.foo = foo;
// I have this.foo, we should watch for changes here.
Observable. // ???
.debounceTime(1000)
.subscribe(input => fooService.autoSave(this.foo));
},
error => console.log(error)
);
}
}
How can I subscribe to changes to the Foo
object, and send it up to my server?
Every example I have seen so far involves either having foo
come off of an Input()
, or being bound to a form. I just want to watch a plain old Javascript object for changes and react to that.
I have tried once again, and have been able to debounce an internal primitive property on a component here.
But, I have been unable to make this work with a complex object that has properties of its own (here); as the setter in the provided plnkr is not called by ngModel
when it updates the property of the foo
object.
A correct answer will demonstrate this working with a complex object.
I believe I have it working with a complex object; but one must ensure the object is immutable and that you have setters for each property, which is kind of a letdown. It also appears to require the splitting of [(ngModel)]
into [ngModel]
and (ngModelChange)
so you can specify custom setter logic.
You could theoretically abstract the functionality to a state service, but I'd like to see if the amount of boilerplate can be stripped down further. Creating new objects each time you would like a state change is kind of frustrating.
I appear to have partially solved this for simple & complex objects. If one has no need for an entire change detector, you can implement this easily with just:
Below is a very minimal example of how one could accomplish this.
import {Component} from '@angular/core'
import {Subject} from 'rxjs/Rx';
@Component({
selector: 'my-app',
providers: [],
template: `
<div>
<input type="text" [(ngModel)]="foo" />
<div>current Foo: {{ foo }}</div>
<div>debounced Foo: {{ debouncedFoo }}</div>
</div>
`,
directives: []
})
export class App {
private _foo: any;
debouncedFoo: any;
fooSubject : Subject<any> = new Subject<any>(this._foo);
fooStream : Observable<any> = this.fooSubject.asObservable();
get foo() {
return this._foo;
});
set foo(value:any) {
this._foo = value;
this.fooSubject.next(value);
});
constructor() {
// Manipulate your subscription stream here
this.subscription = this.fooStream
.debounceTime(2000)
.subscribe(
value => {
this.debouncedFoo = value;
});
}
}
http://plnkr.co/edit/HMJz6UWeZIBovMkXlR01?p=preview
Of course, realistically the component should have no knowledge of the subject or observable, so extract them to a service as you please.
This example only works with primitives. If you would like it to work with objects, you need custom setter logic for each property, and remember to create new objects each time you alter a property of that object.
@vjawala is very close :)
If you want to track input (annotated with @Input
) properties use OnChanges
:
ngOnChanges(changes: SimpleChanges) {
if(changes['foo']){
triggerAutoSave();
}
}
If you want to track key/value object, then implement DoCheck
listener and use KeyValueDiffer
, example is provided in angular docs.
It might be much more efficient/simple to subscribe directly to change/input (most likely both) events:
<form (input)="triggerAutoSave()" (change)="triggerAutoSave()">
<input>
<input>
<input>
</form>
Debounce is relatively easy with RxJS:
triggerAutoSave(){
subject.next(this.foo);
}
subject.debounceTime(500).subscribe(...);
If you are saving via http then exhaustMap
might help.
What if you use the ngDoCheck()
function? Something like:
@Component({
selector: 'foo',
templateUrl: '/angular/views/foo.template.html',
directives: [ContentEditableDirective, ROUTER_DIRECTIVES],
providers: [FooService]
})
export class FooComponent implements OnInit, DoCheck {
public foo: Foo = new Foo();
constructor(
private fooService: FooService,
private route : ActivatedRoute,
private router : Router) {}
ngOnInit() {
let id = +this.route.snapshot.params['id'];
this.foo = this.fooService.getFoo(id);
}
ngDoCheck() {
fooService.autoSave(this.foo);
}
}
This is something I just came up with. It probably needs a little polishing, but it works well regardless.
Working Plunker for example usage
export class ChangeTracker {
private _object: any;
private _objectRetriever: () => any;
private _objectSubject: Subject<any> = new Subject<any>();
private _onChange: (obj: any) => void;
private _fnCompare: (a: any, b: any) => boolean;
constructor(objRetriever: () => any,
onChange: (obj: any) => void,
fnCompare?: (a: any, b: any) => boolean) {
this._object = objRetriever();
this._objectRetriever = objRetriever;
this._onChange = onChange;
this._fnCompare = fnCompare ? fnCompare : this.defaultComparer;
this._objectSubject
.debounceTime(1000)
.subscribe((data: any) => {
this._onChange(data);
});
setInterval(() => this.detectChanges(), 500);
}
private defaultComparer(a: any, b: any) {
return JSON.stringify(a) == JSON.stringify(b);
}
private detectChanges() {
let currentObject = this._objectRetriever();
if (!this._fnCompare(this._object, currentObject)) {
this._object = currentObject;
this._objectSubject.next(currentObject);
}
}
}
Usage in your code sample:
@Component({
selector: 'foo',
templateUrl: '/angular/views/foo.template.html',
directives: [ContentEditableDirective, ROUTER_DIRECTIVES],
providers: [FooService]
})
export class FooComponent implements OnInit {
public foo: Foo = new Foo();
constructor(
private fooService: FooService,
private route : ActivatedRoute,
private router : Router) {}
ngOnInit() {
let id = +this.route.snapshot.params['id'];
this.fooService.getFoo(id).subscribe(
foo => {
this.foo = foo;
/* just create a new ChangeTracker with a reference
* to the local foo variable and define the action to be
* taken on a detected change
*/
new ChangeTracker(
() => this.foo,
(value) => {
this.fooService.autoSave(value);
}
);
},
error => console.log(error)
);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With