<!DOCTYPE html>
<html>
<head>
<title>Angular 4 - Watcher Service Example</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous" />
</head>
<body>
<my-app>
Loading...
</my-app>
<script src="https://unpkg.com/zone.js@0.8.4/dist/zone.js"></script>
<script src="https://unpkg.com/reflect-metadata@0.1.9/Reflect.js"></script>
<script src="https://unpkg.com/systemjs@0.19.41/dist/system.js"></script>
<script src="https://unpkg.com/typescript@2.1.4/lib/typescript.js"></script>
<script src="config.js"></script>
<script>
System.import('app')
.catch(console.error.bind(console));
</script>
</body>
</html>
System.config({
transpiler: 'typescript',
typescriptOptions: {
emitDecoratorMetadata: true,
experimentalDecorators: true
},
map: {
app: "./src",
'@angular/core': 'npm:@angular/core@4.0.0/bundles/core.umd.js',
'@angular/common': 'npm:@angular/common@4.0.0/bundles/common.umd.js',
'@angular/compiler': 'npm:@angular/compiler@4.0.0/bundles/compiler.umd.js',
'@angular/platform-browser': 'npm:@angular/platform-browser@4.0.0/bundles/platform-browser.umd.js',
'@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic@4.0.0/bundles/platform-browser-dynamic.umd.js',
'@angular/http': 'npm:@angular/http@4.0.0/bundles/http.umd.js',
'@angular/router': 'npm:@angular/router@4.0.0/bundles/router.umd.js',
'@angular/forms': 'npm:@angular/forms@4.0.0/bundles/forms.umd.js',
'@angular/animations': 'npm:@angular/animations@4.0.0/bundles/animations.umd.js',
'@angular/core/testing': 'npm:@angular/core@4.0.0/bundles/core-testing.umd.js',
'@angular/common/testing': 'npm:@angular/common/bundles@4.0.0/common-testing.umd.js',
'@angular/compiler/testing': 'npm:@angular/compiler@4.0.0/bundles/compiler-testing.umd.js',
'@angular/platform-browser/testing': 'npm:@angular/platform-browser@4.0.0/bundles/platform-browser-testing.umd.js',
'@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic@4.0.0/bundles/platform-browser-dynamic-testing.umd.js',
'@angular/http/testing': 'npm:@angular/http@4.0.0/bundles/http-testing.umd.js',
'@angular/router/testing': 'npm:@angular/router@4.0.0/bundles/router-testing.umd.js',
'rxjs': 'npm:rxjs@5.1.0',
'lodash': 'npm:lodash@4.17.4/'
},
//packages defines our app package
packages: {
app: {
main: './main.ts',
defaultExtension: 'ts'
},
rxjs: {
defaultExtension: 'js'
},
lodash: {
defaultExtension: 'js'
}
},
paths: {
'npm:': 'https://unpkg.com/'
}
});
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import {AppComponent} from './app.component';
import {InfoComponent} from './info.component';
import {WatchComponent} from './watch.component';
import {WatchFormGroupService} from './watcher.service'
@NgModule({
imports: [ BrowserModule,
FormsModule,
CommonModule,
ReactiveFormsModule,
],
declarations: [ AppComponent,InfoComponent, WatchComponent],
providers: [WatchFormGroupService]
bootstrap: [ AppComponent ]
})
export class AppModule {}
{
"compilerOptions": {
"baseUrl": "",
"declaration": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": ["es6", "dom"],
"mapRoot": "./",
"module": "es6",
"moduleResolution": "node",
"sourceMap": true,
"target": "es5"
}
}
//our root app component
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import {ROUTER_PROVIDERS, ROUTER_DIRECTIVES, RouteParams, RouteConfig, Router, LocationStrategy, HashLocationStrategy} from '@angular2/router';
import { enableProdMode } from '@angular/core';
import { AppModule } from './app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
import { Component, Output, Input, EventEmitter, OnInit, OnDestroy, AfterViewInit } from '@angular/core';
import { FormGroup, FormBuilder } from '@angular/forms';
@Component({
selector: 'my-app',
templateUrl: 'src/app.template.html',
styleUrls: ['src/app.css']
})
export class AppComponent implements OnInit {
formGroup: FormGroup = new FormGroup({});
constructor() { }
ngOnInit() {
}
}
<div class="header">
<h1>Angular 4 - Watcher Service Example</h1>
</div>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<my-info [formGroup]="formGroup"></my-info>
<h3>Watcher Service is watching...</h3>
<watch-info [formGroup]="formGroup"></watch-info>
</div>
</div>
</div>
import { Injectable } from '@angular/core';
import {AbstractControl, FormControl, FormGroup} from '@angular/forms';
import {Subscription} from "rxjs/Rx";
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Rx';
import * as _ from 'lodash';
export interface FormGroupChange {
path: string;
name: string;
control: FormControl;
}
/*
Allows you to subscribe to value and status changes for fields in a FormGroup. Events are debounced to avoid too many
simultaneous calls, and are emitted even when the validity of a field updates because of dependencies on other fields.
Usage:
let subscription = this.watchFormGroupService.watch(this.formGroup, ['personalInformation.firstName', 'personalInformation.lastName'])
.subscribe((data: FormGroupChange) => {
// ...
});
// Don't forget to unsubscribe! (eg. in ngOnDestroy)
subscription.unsubscribe();
*/
@Injectable()
export class WatchFormGroupService {
private MAX_CHECK_COUNT = 5;
public watch(formGroup: FormGroup, paths: string[], debounce = 400): Observable<FormGroupChange> {
return Observable.create(observer => {
let internalSubs = [];
paths.map(path => {
let control = formGroup.root.get(path);
if(!control) {
let checkCount = 0
let checkAgainInterval = setInterval(() => {
let control = formGroup.root.get(path);
if(control) {
clearInterval(checkAgainInterval);
let eventData: FormGroupChange = {
path: path,
name: _.last(path.split('.')),
control: control
};
let subject = new BehaviorSubject(eventData);
internalSubs.push(Observable.merge(...[control.valueChanges, control.statusChanges, subject]).debounceTime(debounce).map(data => {
observer.next(eventData);
}).subscribe());
}
if(checkCount >= this.MAX_CHECK_COUNT) {
console.warn("NO WATCHER PATH MATCH", path);
clearInterval(checkAgainInterval);
}
checkCount++;
}, 1000);
} else {
let eventData: FormGroupChange = {
path: path,
name: _.last(path.split('.')),
control: control
};
let subject = new BehaviorSubject(eventData);
internalSubs.push(Observable.merge(...[control.valueChanges, control.statusChanges, subject]).debounceTime(debounce).map(data => {
observer.next(eventData);
}).subscribe());
}
});
// Provide a way of canceling and disposing the interval resource
return function unsubscribe() {
_.forEach(internalSubs, sub => {
sub.unsubscribe();
});
};
});
}
}
<div [formGroup]="formGroup.get('myInfo')" >
<div class="form-inline">
<input class="form-control " type="text" formControlName="zipcode" placeholder="zip" name="zipcode">
<input class="form-control" type="text" formControlName="city" placeholder="city" name="city">
<input class="form-control" type="text" formControlName="state" placeholder="state" name="state">
</div>
<div class="form-inline">
<input class="form-control" type="text" formControlName="address1" placeholder="address line 1" name="address1">
<input class="form-control " type="text" formControlName="address2" placeholder="address line 2" name="address2">
<input class="form-control" type="text" formControlName="address3" placeholder="address line 3" name="address3">
</div>
</div>
<br><br>
<p>Address1: {{formData.address1}}</p>
<p>Address2: {{formData.address2}}</p>
<p>Address3: {{formData.address3}}</p>
<p>City, State: {{formData.city}} {{formData.state}}</p>
<p>Zipcode: {{formData.zipcode}}</p>
import { Component, Output, Input, EventEmitter, OnInit, OnDestroy, AfterViewInit } from '@angular/core';
import { FormGroup, FormBuilder } from '@angular/forms';
import {WatchFormGroupService, FormGroupChange} from './watcher.service'
@Component({
selector: 'watch-info',
templateUrl: 'src/watch.template.html'
})
export class WatchComponent implements OnInit, AfterViewInit, OnDestroy {
formData: any = {};
@Input() formGroup: FormGroup;
private watcherSubscription;
constructor(private watchFormGroupService: WatchFormGroupService) {
}
ngOnDestroy(){
this.watcherSubscription.unsubscribe();
}
ngAfterViewInit() {
this.watcherSubscription = this.watchFormGroupService.watch(this.formGroup,
['myInfo.address1','myInfo.address2','myInfo.address3'
,'myInfo.city','myInfo.state','myInfo.zipcode']).subscribe((data: FormGroupChange) => {
this.formData[data.name] = data.control.value;
});
}
ngOnInit() {
}
}
import { Component, Output, Input, EventEmitter, OnInit, OnDestroy, AfterViewInit } from '@angular/core';
import { FormGroup, FormBuilder } from '@angular/forms';
@Component({
selector: 'my-info',
templateUrl: 'src/info.template.html',
styleUrls: ['src/app.css']
})
export class InfoComponent implements OnInit {
@Input() formGroup: FormGroup;
address: Address;
constructor() {
this.address = new Address();
}
ngOnInit() {
let fb = new FormBuilder();
let fg = fb.group({
address1: [this.address.address1],
address2: [this.address.address2],
address3: [this.address.address3],
city: [this.address.city],
state: [this.address.state],
zipcode: [this.address.zipcode]
});
this.formGroup.addControl('myInfo', fg);
}
}
export class Address {
public address2: string;
public address3: string;
constructor(public address1 = '', public city = '', public state = '', public zipcode = '') {
}
}