<!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 = '') {
    
  }
}