System.config({
  //use typescript for compilation
  transpiler: 'typescript',
  //typescript compiler options
  typescriptOptions: {
    emitDecoratorMetadata: true
  },
  paths: {
    'npm:': 'https://unpkg.com/'
  },
  //map tells the System loader where to look for things
  map: {
    
    'app': './src',
    
    '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
    '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
    '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
    '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
    '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
    '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
    '@angular/router': 'npm:@angular/router/bundles/router.umd.js',
    '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
    
    '@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js',
    '@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js',
    '@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js',
    '@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js',
    '@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js',
    '@angular/http/testing': 'npm:@angular/http/bundles/http-testing.umd.js',
    '@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js',
    
    'rxjs': 'npm:rxjs',
    'typescript': 'npm:typescript@2.0.2/lib/typescript.js'
  },
  //packages defines our app package
  packages: {
    app: {
      main: './main.ts',
      defaultExtension: 'ts'
    },
    rxjs: {
      defaultExtension: 'js'
    }
  }
});
<!DOCTYPE html>
<html>

  <head>
    <base href="." />
    <title>angular2 playground</title>
    <link rel="stylesheet" href="style.css" />
    <script src="https://unpkg.com/core-js@2.4.1/client/shim.min.js"></script>
    <script src="https://unpkg.com/zone.js/dist/zone.js"></script>
    <script src="https://unpkg.com/zone.js/dist/long-stack-trace-zone.js"></script>
    <script src="https://unpkg.com/reflect-metadata@0.1.3/Reflect.js"></script>
    <script src="https://unpkg.com/systemjs@0.19.31/dist/system.js"></script>
    <script src="config.js"></script>
    <script>
    System.import('app')
      .catch(console.error.bind(console));
  </script>
  </head>

  <body>
    <my-app>
    loading...
  </my-app>
  </body>

</html>
<h1>Angular 4: Template-driven forms</h1>
<form #myForm="ngForm" (ngSubmit)="register(myForm)" countryCity telephoneNumbers novalidate>
  <p> Is "myForm" valid? {{myForm.valid}} </p>

  <div>
    <label>Name</label>
    <input type="text" name="name" #name="ngModel" ngModel required uniqueName>
    <show-errors [control]="name"></show-errors>
  </div>

  <div>
    <label>Birth Year</label>
    <input type="text" name="birthYear" #year="ngModel" ngModel required birthYear>
    <show-errors [control]="year"></show-errors>
  </div>

  <div ngModelGroup="location">
    <h3>Location</h3>
    <div>
      <label>Country</label>
      <input type="text" name="country" #country="ngModel" ngModel required minlength="5" maxlength="30">
      <show-errors [control]="country"></show-errors>
    </div>
    <div>
      <label>City</label>
      <input type="text" name="city" ngModel>
    </div>
  </div>

  <div ngModelGroup="phoneNumbers">
    <h3>Phone numbers</h3>
    <div *ngFor="let phoneId of phoneNumberIds; let i=index;">
      <label>Phone number {{i + 1}}</label>
      <input type="text" name="phoneNumber[{{phoneId}}]" #phoneNumber="ngModel" ngModel required telephoneNumber>
      <button type="button" (click)="remove(i); myForm.control.markAsTouched()">remove</button>
      <show-errors [control]="phoneNumber"></show-errors>
    </div>
    <button type="button" (click)="add(); myForm.control.markAsTouched()">Add phone number</button>
  </div>

  <show-errors [control]="myForm"></show-errors>

  <pre>{{myForm.value | json}}</pre>
  <button type="submit" [disabled]="myForm.invalid || myForm.pending">Register</button>
  <button type="button" (click)="printMyForm()">Print to console</button>
</form>
import { NgForm } from '@angular/forms';
import { Component, ViewChild } from '@angular/core';

@Component({
  selector: 'my-app',
  templateUrl: 'src/app.component.html'
})
export class AppComponent {

  private count = 1;

  phoneNumberIds: number[] = [1];

  @ViewChild('myForm')
  private myForm: NgForm;

  constructor() {
  }

  remove(i: number) {
    this.phoneNumberIds.splice(i, 1);
  }

  add() {
    this.phoneNumberIds.push(++this.count);
  }


  printMyForm() {
    console.log(this.myForm);
  }

  register(myForm: NgForm) {
    console.log('Registration successful.');
    console.log(myForm.value);
  }

}
import { FormsModule } from '@angular/forms';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { ShowErrorsComponent } from './show-errors.component';

import { BirthYearValidatorDirective } from './validators/birth-year-validator.directive';
import { TelephoneNumberFormatValidatorDirective } from './validators/telephone-number-format-validator.directive';
import { CountryCityValidatorDirective } from './validators/country-city-validator.directive';
import { TelephoneNumbersValidatorDirective } from './validators/telephone-numbers-validator.directive';
import { UniqueNameValidatorDirective } from './validators/name-unique-validator.directive';

import { AppComponent } from './app.component';

@NgModule({
  imports: [BrowserModule, FormsModule],
  declarations: [AppComponent, ShowErrorsComponent,
    BirthYearValidatorDirective,
    TelephoneNumberFormatValidatorDirective,
    CountryCityValidatorDirective,
    TelephoneNumbersValidatorDirective,
    UniqueNameValidatorDirective],
  bootstrap: [AppComponent]
})
export class AppModule {
}
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';

platformBrowserDynamic().bootstrapModule(AppModule);
import { Component, Input } from '@angular/core';
import { AbstractControlDirective, AbstractControl } from '@angular/forms';

@Component({
  selector: 'show-errors',
  template: `
    <ul *ngIf="shouldShowErrors()">
      <li style="color: red" *ngFor="let error of listOfErrors()">{{error}}</li>
    </ul>
  `,
})
export class ShowErrorsComponent {

  private static readonly errorMessages = {
    'required': () => 'This field is required',
    'minlength': (params) => 'The min number of characters is ' + params.requiredLength,
    'maxlength': (params) => 'The max allowed number of characters is ' + params.requiredLength,
    'pattern': (params) => 'The required pattern is: ' + params.requiredPattern,
    'years': (params) => params.message,
    'countryCity': (params) => params.message,
    'uniqueName': (params) => params.message,
    'telephoneNumbers': (params) => params.message,
    'telephoneNumber': (params) => params.message
  };

  @Input()
  private control: AbstractControlDirective | AbstractControl;

  shouldShowErrors(): boolean {
    return this.control &&
      this.control.errors &&
      (this.control.dirty || this.control.touched);
  }

  listOfErrors(): string[] {
    return Object.keys(this.control.errors)
      .map(field => this.getMessage(field, this.control.errors[field]));
  }

  private getMessage(type: string, params: any) {
    return ShowErrorsComponent.errorMessages[type](params);
  }

}
import { Directive } from '@angular/core';
import { NG_VALIDATORS, FormControl, Validator, ValidationErrors } from '@angular/forms';


@Directive({
  selector: '[birthYear]',
  providers: [{provide: NG_VALIDATORS, useExisting: BirthYearValidatorDirective, multi: true}]
})
export class BirthYearValidatorDirective implements Validator {

  validate(c: FormControl): ValidationErrors {
    const numValue = Number(c.value);
    const currentYear = new Date().getFullYear();
    const minYear = currentYear - 85;
    const maxYear = currentYear - 18;
    const isValid = !isNaN(numValue) && numValue >= minYear && numValue <= maxYear;
    const message = {
      'years': {
        'message': 'The year must be a valid number between ' + minYear + ' and ' + maxYear
      }
    };
    return isValid ? null : message;
  }
}
import { Directive } from '@angular/core';
import { NG_VALIDATORS, Validator, FormGroup, ValidationErrors } from '@angular/forms';


@Directive({
  selector: '[countryCity]',
  providers: [{provide: NG_VALIDATORS, useExisting: CountryCityValidatorDirective, multi: true}]
})
export class CountryCityValidatorDirective implements Validator {

  validate(form: FormGroup): ValidationErrors {
    const countryControl = form.get('location.country');
    const cityControl = form.get('location.city');

    if (countryControl != null && cityControl != null) {
      const country = countryControl.value;
      const city = cityControl.value;
      let error = null;

      if (country === 'France' && city !== 'Paris') {
        error = 'If the country is France, the city must be Paris';
      }

      const message = {
        'countryCity': {
          'message': error
        }
      };

      return error ? message : null;
    }
  }
}
import { Directive } from '@angular/core';
import { NG_ASYNC_VALIDATORS, Validator, FormControl, ValidationErrors } from '@angular/forms';


@Directive({
  selector: '[uniqueName]',
  providers: [{provide: NG_ASYNC_VALIDATORS, useExisting: UniqueNameValidatorDirective, multi: true}]
})
export class UniqueNameValidatorDirective implements Validator {

  validate(c: FormControl): ValidationErrors {
    const message = {
      'uniqueName': {
        'message': 'The name is not unique'
      }
    };

    return new Promise(resolve => {
      setTimeout(() => {
        resolve(c.value === 'Existing' ? message : null);
      }, 1000);
    });
  }
}
import { Directive } from '@angular/core';
import { NG_VALIDATORS, Validator, FormControl, ValidationErrors } from '@angular/forms';


@Directive({
  selector: '[telephoneNumber]',
  providers: [{provide: NG_VALIDATORS, useExisting: TelephoneNumberFormatValidatorDirective, multi: true}]
})
export class TelephoneNumberFormatValidatorDirective implements Validator {

  validate(c: FormControl): ValidationErrors {
    const isValidPhoneNumber = /^\d{3,3}-\d{3,3}-\d{3,3}$/.test(c.value);
    const message = {
      'telephoneNumber': {
        'message': 'The phone number must be valid (XXX-XXX-XXX, where X is a digit)'
      }
    };
    return isValidPhoneNumber ? null : message;
  }
}
import { Directive } from '@angular/core';
import { NG_VALIDATORS, Validator, FormGroup, ValidationErrors } from '@angular/forms';


@Directive({
  selector: '[telephoneNumbers]',
  providers: [{provide: NG_VALIDATORS, useExisting: TelephoneNumbersValidatorDirective, multi: true}]
})
export class TelephoneNumbersValidatorDirective implements Validator {

  validate(form: FormGroup): ValidationErrors {

    const message = {
      'telephoneNumbers': {
        'message': 'At least one telephone number must be entered'
      }
    };

    const phoneNumbers = <FormGroup> form.get('phoneNumbers');
    const hasPhoneNumbers = phoneNumbers && Object.keys(phoneNumbers.controls).length > 0;

    return hasPhoneNumbers ? null : message;
  }
}
* {
  outline: none;
  font-family: Arial, serif;
  font-size: 15px;
}

h1 {
  font-size: 20px;
}

form, [ngForm], ngForm, [ng-reflect-form], [ngModelGroup], [formGroupName], [formArrayName] {
  margin: 10px 10px 10px 0;
  padding: 10px;
  border-radius: 5px;
  border: 1px solid #46C5F9;
  display: block;
}

label {
  display: inline-block;
  width: 150px;
  margin-left: 10px;
  padding-bottom: 10px;
}

input {
  padding: 3px 4px;
  border-radius: 3px;
  border: 1px solid #DFDFDF;
}

input:focus {
  border: solid 1px #707070;
  box-shadow: 0 0 5px 1px #969696;
}

[rootNestableForm] h3 {
  color: blue;
}

.form-state {
  color:red;
}

.form-state.valid {
  color:green;
}

p {
  margin-left: 10px;
}

pre {
  padding: 10px;
  background-color: #F8F8F9;
  color: #aaa;
  white-space: pre;
  border-radius: 5px;
  font: 13px 'Courier New', Courier, 'Lucida Sans Typewriter', 'Lucida Typewriter', monospace;
}

button {
  box-shadow: inset 0 0 0 0 #54a3f7;
  background: linear-gradient(to bottom, #007dc1 5%, #0061a7 100%);
  border: none;
  border-radius: 6px;
  display: inline-block;
  cursor: pointer;
  color: #ffffff;
  font-weight: bold;
  padding: 3px 25px;
  text-decoration: none;
}

button:hover {
  background: #0061a7 linear-gradient(to bottom, #0061a7 5%, #007dc1 100%);
}

button:active {
  position: relative;
  top: 1px;
}

button[disabled] {
  background: #B9B9B9;
}