<!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>
    
    <link rel="stylesheet" href="https://unpkg.com/font-awesome@4.7.0/css/font-awesome.css">
    <link rel="stylesheet" href="https://unpkg.com/purecss@0.6.1/build/pure-min.css" integrity="sha384-CCTZv2q9I9m3UOxRLaJneXrrqKwUNOzZ6NGEUMwHtShDJ+nCoiXJCAgi05KfkLGY" crossorigin="anonymous">
  </head>

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

</html>
/* Styles go here */

.error-message {
    color:red;
    font-size: .75em;
    margin-left: 10rem;
}

/* it has to be block for Pure (flex by default) */
dorf-field-wrapper {
  display: block !important;
}
### DORF - custom field

Example which shows the way of how to extend existing DORF set of fields.

https://github.com/mat3e/dorf
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',
    
    'dorf': 'npm:dorf@3.0.0',
    
    'rxjs': 'npm:rxjs',
    'typescript': 'npm:typescript@2.0.2/lib/typescript.js'
  },
  //packages defines our app package
  packages: {
    app: {
      main: './main.ts',
      defaultExtension: 'ts'
    },
    dorf: {
      main: "./index.js",
      defaultExtension: "js"
    },
    rxjs: {
      defaultExtension: 'js'
    }
  }
});
//main entry point
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {AppModule} from './app';

platformBrowserDynamic().bootstrapModule(AppModule)
//our root app component
import {Component, NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';

import { DorfModule } from "dorf";

import {
  STAR_TAG,
  StarRatingDefinition,
  StarRatingMetadata,
  StarRatingComponent
} from "./ext/star-rating.component";
import { Person } from './person/model';
import { PersonDetailComponent } from "./person/person-detail.component";

@Component({
  selector: 'my-app',
  template: `
    <h2>Person in form: {{activeIndex >= 0 ? activePerson.fullname : "new person"}}</h2>
    <person-details [domainObject]="activePerson" (createUpdate)=onCreateUpdate($event)></person-details>
    <hr/>
    <table class="pure-table pure-table-striped">
    <tbody>
      <tr *ngFor="let p of people; let i = index;">
        <td>{{p.fullname}}</td>
        <td><button (click)=onClick(i) class="pure-button">edit</button></td>
      </tr>
    </tbody>
    </table>
  `,
})
export class App {
  activeIndex = -1;
  activePerson = new Person();

  people: Person[] = [];

  onClick(index: number) {
    this.activeIndex = index;
    this.activePerson = this.people[index];
  }

  onCreateUpdate(person: IPerson) {
    this.activeIndex >= 0 ? this.people[this.activeIndex] = new Person(person)
      : this.people.push(new Person(person));

    this.reset();
  }

  private reset() {
    this.activeIndex = -1;
    this.activePerson = new Person();
  }
}

@NgModule({
    imports: [BrowserModule, DorfModule.forRoot({
      css: {
        form: 'pure-form pure-form-aligned',
        wrapper: 'pure-control-group',
        error: 'error-message',
        buttons: {
          save: 'pure-button pure-button-primary',
          reset: 'hidden',
          group: 'pure-controls'
        }
      },
      dorfFields: [STAR_TAG]
    })],
    declarations: [App, StarRatingComponent, PersonDetailComponent],
    bootstrap: [App]
})
export class AppModule { }
.rating .fa.fa-star {
  color: black;
}

.rating .fa.fa-star.active {
  color: #DCAA00;
}

.rating:hover .fa.fa-star {
  cursor: pointer;
  color: #DCAA00;
}

.rating .fa.fa-star:active {
  color: red;
}

.rating .fa.fa-star:hover~.fa.fa-star {
  color: black;
}
import { Component } from "@angular/core";
import { FormControl } from "@angular/forms";

import {
    DorfConfigService,
    IDorfFieldDefinition,
    IDorfFieldMetadata,
    DorfFieldDefinition,
    createPropertyDecorator,
    DorfFieldMetadata,
    AbstractDorfFieldComponent
} from "dorf";

/**
 * First - extend a standard interface.
 */
export interface IStarRatingDefinition extends IDorfFieldDefinition<number> {
    max: number;
}

/**
 * Second - extend standard field definition and implement the above interface.
 */
export class StarRatingDefinition extends DorfFieldDefinition<number> implements IStarRatingDefinition {
    static get TAG() {
        return "star-rating";
    }

    private _max = 5;

    constructor(options?: IStarRatingDefinition<T>) {
        super(options);

        if (options) {
            this._max = options.max > 1 ? options.max : this._max;
        }
    }

    get max() { return this._max; }

    get tag() { return StarRatingDefinition.TAG; }
}

/**
 * Optional - create a decorator.
 */
export function DorfStars(options: IStarRatingDefinition) {
    return createPropertyDecorator<T>(new StarRatingDefinition(options));
}

/**
 * Third - extend standard metadata and implement the above interface.
 */
export class StarRatingMetadata extends DorfFieldMetadata<number, StarRatingDefinition> implements IStarRatingDefinition {
    constructor(definition = new StarRatingDefinition<T>(), options?: IDorfFieldMetadata<T>) {
        super(definition, options);
    }

    get max() { return this.definition.max; }
}

/**
 * Last but not least - create component which consumes the above metadata
 * (extension of an abstract component + implementation of the above interface).
 */
@Component({
  selector: StarRatingDefinition.TAG,
  styleUrls: ["src/ext/star-rating.component.css"],
  templateUrl: "src/ext/star-rating.component.html"
})
export class StarRatingComponent extends AbstractDorfFieldComponent<number, StarRatingMetadata> implements IStarRatingDefinition {

    constructor(config: DorfConfigService) {
        super(config);
    }

    setValue(val: number) {
        // 1 more than array index
        this.formControl.setValue(val + 1);
    }

    get max() { return this.metadata.max; }

    get stars() { return new Array(this.max); }
    get value() { return this.formControl.value; }
}

export const STAR_TAG: DorfTag<typeof StarRatingDefinition, typeof StarRatingMetadata> = {
    tag: StarRatingDefinition.TAG,
    definition: StarRatingDefinition,
    metadata: StarRatingMetadata
}
<span class="rating">
    <input type="hidden" [id]="key" [name]="key" [formControl]="formControl" />
    <span *ngFor="let star of stars; let i=index;" class="fa fa-star" [ngClass]="{'active': (i < value)}" (click)="setValue(i)"></span>
</span>
import { Validators } from "@angular/forms";

import { DorfObject, DorfInput, DorfRadio, DorfSelect, DorfCheckbox } from "dorf";

import { DorfStars } from "../ext/star-rating.component";

export interface IPerson {
  name: string;
  surname: string;
  rating: number;
}

@DorfObject()
export class Person implements IPerson {
  
  @DorfInput<string>({
    label: "Name",
    type: "text",
    validator: Validators.required,
    errorMessage: "Name is required"
  })
  name: string;
  
  @DorfInput<string>({
    label: "Surname", 
    type: "text",
    validator: Validators.required, 
    errorMessage: "Surname is required"
  })
  surname: string;
  
  @DorfStars({
    label: "Rating",
    max: 7
  })
  rating: number;

  get fullname() {
      return this.name + " " + this.surname;
  }

  constructor(base: IPerson = null) {
    if (base) {
      this.name = base.name;
      this.surname = base.surname;
      this.rating = base.rating;
    }
  }
}
import { Component, Output, EventEmitter } from '@angular/core';
import { PropertiesToDorfDefinitionsMap, IDorfForm, DorfForm, DorfObjectInput, DorfConfigService } from 'dorf';

import { IPerson, Person } from './model';
import { STAR_TAG } from '../ext/star-rating.component';

/**
 * Lightweight class which creates a form.
 * Template provided from the library, but it is possible to prowide a custom one (which uses Dorf components).
 *
 * It's important to pass config in the constructor and define onDorfSubmit method.
 */
@DorfForm({
    additionalTags: [STAR_TAG]
})
@Component({
    selector: 'person-details'
})
export class PersonDetailComponent implements IDorfForm {
    @DorfObjectInput() domainObject: Person;
    @Output() createUpdate = new EventEmitter<IPerson>();

    constructor(public config: DorfConfigService) { }

    onDorfSubmit() {
        let result = this['form'].value as IPerson;

        console.log(result);
        this.createUpdate.emit(result);
    }
}