diff --git a/public/docs/_examples/cb-dynamic-form/ts/app/dynamic-form-question.component.ts b/public/docs/_examples/cb-dynamic-form/ts/app/dynamic-form-question.component.ts
new file mode 100644
index 0000000000..2348e4d8b9
--- /dev/null
+++ b/public/docs/_examples/cb-dynamic-form/ts/app/dynamic-form-question.component.ts
@@ -0,0 +1,14 @@
+// #docregion
+import {Component, Input} from 'angular2/core';
+import {ControlGroup} from 'angular2/common';
+import {QuestionBase} from './question-base';
+
+@Component({
+ selector:'df-question',
+ templateUrl:'app/dynamic-form-question.component.html'
+})
+export class DynamicFormQuestionComponent {
+ @Input() question:QuestionBase;
+ @Input() form:ControlGroup;
+ get isValid() { return this.form.controls[this.question.key].valid; }
+}
diff --git a/public/docs/_examples/cb-dynamic-form/ts/app/dynamic-form.component.html b/public/docs/_examples/cb-dynamic-form/ts/app/dynamic-form.component.html
new file mode 100644
index 0000000000..7cb4b08ce3
--- /dev/null
+++ b/public/docs/_examples/cb-dynamic-form/ts/app/dynamic-form.component.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+ Saved the following values {{payLoad}}
+
+
diff --git a/public/docs/_examples/cb-dynamic-form/ts/app/dynamic-form.component.ts b/public/docs/_examples/cb-dynamic-form/ts/app/dynamic-form.component.ts
new file mode 100644
index 0000000000..aac95acb39
--- /dev/null
+++ b/public/docs/_examples/cb-dynamic-form/ts/app/dynamic-form.component.ts
@@ -0,0 +1,30 @@
+// #docregion
+import {Component, Input, OnInit} from 'angular2/core';
+import {ControlGroup} from 'angular2/common';
+
+import {QuestionBase} from './question-base';
+import {QuestionControlService} from './question-control.service';
+import {DynamicFormQuestionComponent} from './dynamic-form-question.component';
+
+@Component({
+ selector:'dynamic-form',
+ templateUrl:'app/dynamic-form.component.html',
+ directives: [DynamicFormQuestionComponent],
+ providers: [QuestionControlService]
+})
+export class DynamicForm {
+
+ @Input() questions: QuestionBase[] = [];
+ form: ControlGroup;
+ payLoad = '';
+
+ constructor(private _qcs: QuestionControlService) { }
+
+ ngOnInit(){
+ this.form = this._qcs.toControlGroup(this.questions);
+ }
+
+ onSubmit() {
+ this.payLoad = JSON.stringify(this.form.value);
+ }
+}
diff --git a/public/docs/_examples/cb-dynamic-form/ts/app/main.ts b/public/docs/_examples/cb-dynamic-form/ts/app/main.ts
new file mode 100644
index 0000000000..dd43b98095
--- /dev/null
+++ b/public/docs/_examples/cb-dynamic-form/ts/app/main.ts
@@ -0,0 +1,5 @@
+import {bootstrap} from 'angular2/platform/browser';
+import {AppComponent} from './app.component';
+
+bootstrap(AppComponent, [])
+ .catch((err:any) => console.error(err));
diff --git a/public/docs/_examples/cb-dynamic-form/ts/app/question-base.ts b/public/docs/_examples/cb-dynamic-form/ts/app/question-base.ts
new file mode 100644
index 0000000000..c22de02989
--- /dev/null
+++ b/public/docs/_examples/cb-dynamic-form/ts/app/question-base.ts
@@ -0,0 +1,25 @@
+// #docregion
+export class QuestionBase{
+ value:T;
+ key:string;
+ label:string;
+ required:boolean;
+ order:number;
+ controlType:string;
+
+ constructor(options:{
+ value?:T,
+ key?:string,
+ label?:string,
+ required?:boolean,
+ order?:number,
+ controlType?:string
+ } = {}){
+ this.value = options.value;
+ this.key = options.key || '';
+ this.label = options.label || '';
+ this.required = !!options.required;
+ this.order = options.order === undefined ? 1 : options.order;
+ this.controlType = options.controlType || '';
+ }
+}
diff --git a/public/docs/_examples/cb-dynamic-form/ts/app/question-control.service.ts b/public/docs/_examples/cb-dynamic-form/ts/app/question-control.service.ts
new file mode 100644
index 0000000000..cc7ebc751a
--- /dev/null
+++ b/public/docs/_examples/cb-dynamic-form/ts/app/question-control.service.ts
@@ -0,0 +1,18 @@
+// #docregion
+import {Injectable} from 'angular2/core';
+import {ControlGroup, FormBuilder, Validators} from 'angular2/common';
+import {QuestionBase} from './question-base';
+
+@Injectable()
+export class QuestionControlService {
+ constructor(private _fb:FormBuilder){ }
+
+ toControlGroup(questions:QuestionBase[] ) {
+ let group = {};
+
+ questions.forEach(question => {
+ group[question.key] = question.required ? [question.value || '', Validators.required] : [];
+ });
+ return this._fb.group(group);
+ }
+}
diff --git a/public/docs/_examples/cb-dynamic-form/ts/app/question-dropdown.ts b/public/docs/_examples/cb-dynamic-form/ts/app/question-dropdown.ts
new file mode 100644
index 0000000000..1c3ca2807a
--- /dev/null
+++ b/public/docs/_examples/cb-dynamic-form/ts/app/question-dropdown.ts
@@ -0,0 +1,12 @@
+// #docregion
+import {QuestionBase} from './question-base';
+
+export class DropdownQuestion extends QuestionBase{
+ controlType = 'dropdown';
+ options:{key:string, value:string}[] = [];
+
+ constructor(options:{} = {}){
+ super(options);
+ this.options = options['options'] || [];
+ }
+}
diff --git a/public/docs/_examples/cb-dynamic-form/ts/app/question-textbox.ts b/public/docs/_examples/cb-dynamic-form/ts/app/question-textbox.ts
new file mode 100644
index 0000000000..573209d944
--- /dev/null
+++ b/public/docs/_examples/cb-dynamic-form/ts/app/question-textbox.ts
@@ -0,0 +1,12 @@
+// #docregion
+import {QuestionBase} from './question-base';
+
+export class TextboxQuestion extends QuestionBase{
+ controlType = 'textbox';
+ type:string;
+
+ constructor(options:{} = {}){
+ super(options);
+ this.type = options['type'] || '';
+ }
+}
diff --git a/public/docs/_examples/cb-dynamic-form/ts/app/question.service.ts b/public/docs/_examples/cb-dynamic-form/ts/app/question.service.ts
new file mode 100644
index 0000000000..0b805758f4
--- /dev/null
+++ b/public/docs/_examples/cb-dynamic-form/ts/app/question.service.ts
@@ -0,0 +1,47 @@
+// #docregion
+import {Injectable} from 'angular2/core';
+import {QuestionBase} from './question-base';
+import {DynamicForm} from './dynamic-form.component';
+import {TextboxQuestion} from './question-textbox';
+import {DropdownQuestion} from './question-dropdown';
+
+@Injectable()
+export class QuestionService {
+
+ // Todo: get from a remote source of question metadata
+ // Todo: make asynchronous
+ getQuestions() {
+
+ let questions:QuestionBase[] = [
+
+ new DropdownQuestion({
+ key:'brave',
+ label: 'Bravery Rating',
+ options: [
+ {key:'solid', value:'Solid'},
+ {key:'great', value:'Great'},
+ {key:'good', value:'Good'},
+ {key:'unproven',value:'Unproven'}
+ ],
+ order: 3
+ }),
+
+ new TextboxQuestion({
+ key:'firstName',
+ label:'First name',
+ value:'Bombasto',
+ required: true,
+ order: 1
+ }),
+
+ new TextboxQuestion({
+ key:'emailAddress',
+ label:'Email',
+ type: 'email',
+ order: 2
+ })
+ ];
+
+ return questions.sort((a,b) => a.order - b.order);
+ }
+}
diff --git a/public/docs/_examples/cb-dynamic-form/ts/example-config.json b/public/docs/_examples/cb-dynamic-form/ts/example-config.json
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/public/docs/_examples/cb-dynamic-form/ts/index.html b/public/docs/_examples/cb-dynamic-form/ts/index.html
new file mode 100644
index 0000000000..a5874bff3f
--- /dev/null
+++ b/public/docs/_examples/cb-dynamic-form/ts/index.html
@@ -0,0 +1,39 @@
+
+
+
+
+ Dynamic Form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Loading app...
+
+
+
diff --git a/public/docs/_examples/cb-dynamic-form/ts/plnkr.json b/public/docs/_examples/cb-dynamic-form/ts/plnkr.json
new file mode 100644
index 0000000000..faa498f46f
--- /dev/null
+++ b/public/docs/_examples/cb-dynamic-form/ts/plnkr.json
@@ -0,0 +1,9 @@
+{
+ "description": "Dynamic Form",
+ "files":[
+ "!**/*.d.ts",
+ "!**/*.js",
+ "!**/*.[1].*"
+ ],
+ "tags":["cookbook"]
+}
diff --git a/public/docs/_examples/cb-dynamic-form/ts/sample.css b/public/docs/_examples/cb-dynamic-form/ts/sample.css
new file mode 100644
index 0000000000..fe2cc28481
--- /dev/null
+++ b/public/docs/_examples/cb-dynamic-form/ts/sample.css
@@ -0,0 +1,7 @@
+.errorMessage{
+ color:red;
+}
+
+.form-row{
+ margin-top: 10px;
+}
\ No newline at end of file
diff --git a/public/docs/ts/latest/cookbook/_data.json b/public/docs/ts/latest/cookbook/_data.json
index 5dfbf9c992..31e71596e4 100644
--- a/public/docs/ts/latest/cookbook/_data.json
+++ b/public/docs/ts/latest/cookbook/_data.json
@@ -14,5 +14,10 @@
"component-communication": {
"title": "Component Interaction",
"description": "Share information between different directives and components"
+ },
+
+ "dynamic-form": {
+ "title": "Dynamic Form",
+ "description": "Render dynamic forms with NgFormModel"
}
}
diff --git a/public/docs/ts/latest/cookbook/dynamic-form.jade b/public/docs/ts/latest/cookbook/dynamic-form.jade
new file mode 100644
index 0000000000..e196074b66
--- /dev/null
+++ b/public/docs/ts/latest/cookbook/dynamic-form.jade
@@ -0,0 +1,143 @@
+include ../_util-fns
+
+:marked
+ We can't always justify the cost and time to build handcrafted forms,
+ especially if we'll need a great number of them, they're similar to each other, and they change frequently
+ to meet rapidly changing business and regulatory requirements.
+
+ It may be more economical to create the forms dynamically, based on metadata that describe the business object model.
+
+ In this cookbook we show how to use `ngFormModel` to dynamically to render a simple form with different control types and validation.
+ It's a primitive start.
+ It might evolve to support a much richer variety of questions, more graceful rendering, and superior user experience.
+ All such greatness has humble beginnings.
+
+ Our example we use a dynamic form to build an online application experience for heroes seeking employment.
+ The agency is constantly tinkering with the application process.
+ We can create the forms on the fly *without changing our application code*.
+
+
+:marked
+ ## Table of contents
+
+ [Question Model](#object-model)
+
+ [Form Component](#form-component)
+
+ [Questionnaire Metadata](#questionnaire-metadata)
+
+ [Dynamic Template](#dynamic-template)
+
+:marked
+ **See the [live example](/resources/live-examples/cb-dynamic-form/ts/plnkr.html)**.
+
+.l-main-section
+
+:marked
+ ## Question Model
+
+ The first step is to define an object model that can describe all scenarios needed by the form functionality.
+ The hero application process involves a form with a lot of questions.
+ The "question" is the most fundamental object in the model.
+
+ We have created `QuestionBase` as the most fundamental question class.
+
++makeExample('cb-dynamic-form/ts/app/question-base.ts','','app/question-base.ts')
+
+:marked
+ From this base we derived two new classes in `TextboxQuestion` and `DropdownQuestion` that represent Textbox and Dropdown questions.
+ The idea is that the form will be bound to specific question types and render the appropriate controls dynamically.
+
+ `TextboxQuestion` supports multiple html5 types like text, email, url etc via the `type` property.
+
++makeExample('cb-dynamic-form/ts/app/question-textbox.ts',null,'app/question-textbox.ts')(format='.')
+
+:marked
+ `DropdownQuestion` presents a list of choices in a select box.
+
++makeExample('cb-dynamic-form/ts/app/question-dropdown.ts',null,'app/question-dropdown.ts')(format='.')
+
+:marked
+ Next we have defined `QuestionControlService`, a simple service for transforming our questions to an ngForm control group.
+ In a nutshell, the control group consumes the metadata from the question model and allows us to specify default values and validation rules.
+
++makeExample('cb-dynamic-form/ts/app/question-control.service.ts',null,'app/question-control.service.ts')(format='.')
+
+
+:marked
+ ## Question form components
+ Now that we have defined the complete model we are ready to create components to represent the dynamic form.
+
+:marked
+ `DynamicForm` is the entry point and the main container for the form.
++makeTabs(
+ `cb-dynamic-form/ts/app/dynamic-form.component.html,
+ cb-dynamic-form/ts/app/dynamic-form.component.ts`,
+ null,
+ `dynamic-form.component.html,
+ dynamic-form.component.ts`
+)
+:marked
+ It presents a list of questions, each question bound to a `` component element.
+ The `` tag matches the `DynamicFormQuestionComponent`,
+ the component responsible for rendering the details of each _individual_ question based on values in the data-bound question object.
+
++makeTabs(
+ `cb-dynamic-form/ts/app/dynamic-form-question.component.html,
+ cb-dynamic-form/ts/app/dynamic-form-question.component.ts`,
+ null,
+ `dynamic-form-question.component.html,
+ dynamic-form-question.component.ts`
+)
+:marked
+ Notice this component can present any type of question in our model.
+ We only have two types of questions at this point but we can imagine many more.
+ The `ngSwitch` determines which type of question to display.
+
+ In both components we're relying on Angular's **ngFormMode** to connect the template HTML to the
+ underlying control objects, populated from the question model with display and validation rules.
+
+
+:marked
+ ## Questionnaire data
+:marked
+ `DynamicForm` expects the list of questions in the form of an array bound to `@Input() questions`.
+
+ The set of questions we have defined for the job application is returned from the `QuestionService`.
+ In a real app we'd retrieve these questions from storage.
+
+ The key point is that we control the hero job application questions entirely through the objects returned from `QuestionService`.
+ Questionnaire maintenance is a simple matter of adding, updating, and removing objects from the `questions` array.
+
++makeExample('cb-dynamic-form/ts/app/question.service.ts','','app/question.service.ts')
+
+:marked
+ Finally, we display an instance of the form in the `AppComponent` shell.
+
++makeExample('cb-dynamic-form/ts/app/app.component.ts','','app.component.ts')
+
+
+:marked
+ ## Dynamic Template
+ Although in this example we're model a job application for heroes, there are no references to any specific hero question
+ outside the objects returned by `QuestionService`.
+
+ This is very important since it allows us to repurpose the components for any type of survey
+ as long as it's compatible with our *question* object model.
+ The key is the dynamic data binding of metadata used to render the form
+ without making any hardcoded assumptions about specific questions.
+ In addition to control metadata, we are also adding validation dynamically.
+
+ The *Save* button is disabled until the form is in a valid state.
+ When the form is valid, we can click *Save* and the app renders the current form values as JSON.
+ This proves that any user input is bound back to the data model.
+ Saving and retrieving the data is an exercise for another time.
+
+:marked
+ The final form looks like this:
+figure.image-display
+ img(src="/resources/images/cookbooks/dynamic-form/dynamic-form.png" alt="Dynamic-Form")
+
+
+:marked
+ [Back to top](#top)
\ No newline at end of file
diff --git a/public/resources/images/cookbooks/dynamic-form/dynamic-form.png b/public/resources/images/cookbooks/dynamic-form/dynamic-form.png
new file mode 100644
index 0000000000..54b680a672
Binary files /dev/null and b/public/resources/images/cookbooks/dynamic-form/dynamic-form.png differ