docs(dynamic-cookbook): upgrade to use new forms api
convert exiting to use deprecated name converted to new api text warnings fix plunker text test weak text space text lint order tweak
This commit is contained in:
parent
749c5eae1b
commit
3cb219c5ab
|
@ -0,0 +1,27 @@
|
|||
/// <reference path='../_protractor/e2e.d.ts' />
|
||||
'use strict';
|
||||
/* tslint:disable:quotemark */
|
||||
describe('Dynamic Form Deprecated', function () {
|
||||
|
||||
beforeAll(function () {
|
||||
browser.get('');
|
||||
});
|
||||
|
||||
it('should submit form', function () {
|
||||
let firstNameElement = element.all(by.css('input[id=firstName]')).get(0);
|
||||
expect(firstNameElement.getAttribute('value')).toEqual('Bombasto');
|
||||
|
||||
let emailElement = element.all(by.css('input[id=emailAddress]')).get(0);
|
||||
let email = 'test@test.com';
|
||||
emailElement.sendKeys(email);
|
||||
expect(emailElement.getAttribute('value')).toEqual(email);
|
||||
|
||||
element(by.css('select option[value="solid"]')).click();
|
||||
|
||||
let saveButton = element.all(by.css('button')).get(0);
|
||||
saveButton.click().then(function(){
|
||||
expect(element(by.xpath("//strong[contains(text(),'Saved the following values')]")).isPresent()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
// #docregion
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { DynamicFormComponent } from './dynamic-form.component';
|
||||
import { QuestionService } from './question.service';
|
||||
|
||||
@Component({
|
||||
selector: 'my-app',
|
||||
template: `
|
||||
<div>
|
||||
<h2>Job Application for Heroes</h2>
|
||||
<dynamic-form [questions]="questions"></dynamic-form>
|
||||
</div>
|
||||
`,
|
||||
directives: [DynamicFormComponent],
|
||||
providers: [QuestionService]
|
||||
})
|
||||
export class AppComponent {
|
||||
questions: any[];
|
||||
|
||||
constructor(service: QuestionService) {
|
||||
this.questions = service.getQuestions();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
<!-- #docregion -->
|
||||
<div [ngFormModel]="form">
|
||||
<label [attr.for]="question.key">{{question.label}}</label>
|
||||
|
||||
<div [ngSwitch]="question.controlType">
|
||||
|
||||
<input *ngSwitchWhen="'textbox'" [ngControl]="question.key"
|
||||
[id]="question.key" [type]="question.type">
|
||||
|
||||
<select [id]="question.key" *ngSwitchWhen="'dropdown'" [ngControl]="question.key">
|
||||
<option *ngFor="let opt of question.options" [value]="opt.key">{{opt.value}}</option>
|
||||
</select>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="errorMessage" *ngIf="!isValid">{{question.label}} is required</div>
|
||||
</div>
|
|
@ -0,0 +1,15 @@
|
|||
// #docregion
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { ControlGroup } from '@angular/common';
|
||||
|
||||
import { QuestionBase } from './question-base';
|
||||
|
||||
@Component({
|
||||
selector: 'df-question',
|
||||
templateUrl: 'app/dynamic-form-question.component.html'
|
||||
})
|
||||
export class DynamicFormQuestionComponent {
|
||||
@Input() question: QuestionBase<any>;
|
||||
@Input() form: ControlGroup;
|
||||
get isValid() { return this.form.controls[this.question.key].valid; }
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
<!-- #docregion -->
|
||||
<div>
|
||||
<form (ngSubmit)="onSubmit()" [ngFormModel]="form">
|
||||
|
||||
<div *ngFor="let question of questions" class="form-row">
|
||||
<df-question [question]="question" [form]="form"></df-question>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<button type="submit" [disabled]="!form.valid">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div *ngIf="payLoad" class="form-row">
|
||||
<strong>Saved the following values</strong><br>{{payLoad}}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,30 @@
|
|||
// #docregion
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { ControlGroup } from '@angular/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 DynamicFormComponent implements OnInit {
|
||||
|
||||
@Input() questions: QuestionBase<any>[] = [];
|
||||
form: ControlGroup;
|
||||
payLoad = '';
|
||||
|
||||
constructor(private qcs: QuestionControlService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.form = this.qcs.toControlGroup(this.questions);
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
this.payLoad = JSON.stringify(this.form.value);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { bootstrap } from '@angular/platform-browser-dynamic';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
bootstrap(AppComponent, [])
|
||||
.catch((err: any) => console.error(err));
|
|
@ -0,0 +1,25 @@
|
|||
// #docregion
|
||||
export class QuestionBase<T>{
|
||||
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 || '';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
// #docregion
|
||||
import { Injectable } from '@angular/core';
|
||||
import { FormBuilder, Validators } from '@angular/common';
|
||||
import { QuestionBase } from './question-base';
|
||||
|
||||
@Injectable()
|
||||
export class QuestionControlService {
|
||||
constructor(private fb: FormBuilder) { }
|
||||
|
||||
toControlGroup(questions: QuestionBase<any>[] ) {
|
||||
let group = {};
|
||||
|
||||
questions.forEach(question => {
|
||||
group[question.key] = question.required ? [question.value || '', Validators.required] : [question.value || ''];
|
||||
});
|
||||
return this.fb.group(group);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
// #docregion
|
||||
import { QuestionBase } from './question-base';
|
||||
|
||||
export class DropdownQuestion extends QuestionBase<string> {
|
||||
controlType = 'dropdown';
|
||||
options: {key: string, value: string}[] = [];
|
||||
|
||||
constructor(options: {} = {}) {
|
||||
super(options);
|
||||
this.options = options['options'] || [];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
// #docregion
|
||||
import { QuestionBase } from './question-base';
|
||||
|
||||
export class TextboxQuestion extends QuestionBase<string> {
|
||||
controlType = 'textbox';
|
||||
type: string;
|
||||
|
||||
constructor(options: {} = {}) {
|
||||
super(options);
|
||||
this.type = options['type'] || '';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
// #docregion
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { QuestionBase } from './question-base';
|
||||
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<any>[] = [
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<base href="/">
|
||||
<title>Dynamic Form</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<!-- #docregion style -->
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="stylesheet" href="sample.css">
|
||||
<!-- #enddocregion style -->
|
||||
|
||||
<!-- Polyfill(s) for older browsers -->
|
||||
<script src="node_modules/core-js/client/shim.min.js"></script>
|
||||
|
||||
<script src="node_modules/zone.js/dist/zone.js"></script>
|
||||
<script src="node_modules/reflect-metadata/Reflect.js"></script>
|
||||
<script src="node_modules/systemjs/dist/system.src.js"></script>
|
||||
|
||||
<script src="systemjs.config.js"></script>
|
||||
<script>
|
||||
System.import('app').catch(function(err){ console.error(err); });
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<my-app>Loading app...</my-app>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"description": "Dynamic Form Deprecated",
|
||||
"files":[
|
||||
"!**/*.d.ts",
|
||||
"!**/*.js",
|
||||
"!**/*.[1].*"
|
||||
],
|
||||
"tags":["cookbook"]
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
.errorMessage{
|
||||
color:red;
|
||||
}
|
||||
|
||||
.form-row{
|
||||
margin-top: 10px;
|
||||
}
|
|
@ -1,17 +1,17 @@
|
|||
<!-- #docregion -->
|
||||
<div [ngFormModel]="form">
|
||||
<div [formGroup]="form">
|
||||
<label [attr.for]="question.key">{{question.label}}</label>
|
||||
|
||||
<div [ngSwitch]="question.controlType">
|
||||
|
||||
<input *ngSwitchWhen="'textbox'" [ngControl]="question.key"
|
||||
<input *ngSwitchCase="'textbox'" [formControlName]="question.key"
|
||||
[id]="question.key" [type]="question.type">
|
||||
|
||||
<select [id]="question.key" *ngSwitchWhen="'dropdown'" [ngControl]="question.key">
|
||||
<select [id]="question.key" *ngSwitchCase="'dropdown'" [formControlName]="question.key">
|
||||
<option *ngFor="let opt of question.options" [value]="opt.key">{{opt.value}}</option>
|
||||
</select>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="errorMessage" *ngIf="!isValid">{{question.label}} is required</div>
|
||||
</div>
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
// #docregion
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { ControlGroup } from '@angular/common';
|
||||
import { FormGroup, REACTIVE_FORM_DIRECTIVES } from '@angular/forms';
|
||||
|
||||
import { QuestionBase } from './question-base';
|
||||
|
||||
@Component({
|
||||
selector: 'df-question',
|
||||
templateUrl: 'app/dynamic-form-question.component.html'
|
||||
templateUrl: 'app/dynamic-form-question.component.html',
|
||||
directives: [REACTIVE_FORM_DIRECTIVES]
|
||||
})
|
||||
export class DynamicFormQuestionComponent {
|
||||
@Input() question: QuestionBase<any>;
|
||||
@Input() form: ControlGroup;
|
||||
@Input() form: FormGroup;
|
||||
get isValid() { return this.form.controls[this.question.key].valid; }
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<!-- #docregion -->
|
||||
<div>
|
||||
<form (ngSubmit)="onSubmit()" [ngFormModel]="form">
|
||||
<form (ngSubmit)="onSubmit()" [formGroup]="form">
|
||||
|
||||
<div *ngFor="let question of questions" class="form-row">
|
||||
<df-question [question]="question" [form]="form"></df-question>
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
// #docregion
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { ControlGroup } from '@angular/common';
|
||||
import { FormGroup, REACTIVE_FORM_DIRECTIVES } from '@angular/forms';
|
||||
|
||||
import { DynamicFormQuestionComponent } from './dynamic-form-question.component';
|
||||
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],
|
||||
directives: [DynamicFormQuestionComponent, REACTIVE_FORM_DIRECTIVES],
|
||||
providers: [QuestionControlService]
|
||||
})
|
||||
export class DynamicFormComponent implements OnInit {
|
||||
|
||||
@Input() questions: QuestionBase<any>[] = [];
|
||||
form: ControlGroup;
|
||||
form: FormGroup;
|
||||
payLoad = '';
|
||||
|
||||
constructor(private qcs: QuestionControlService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.form = this.qcs.toControlGroup(this.questions);
|
||||
this.form = this.qcs.toFormGroup(this.questions);
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { bootstrap } from '@angular/platform-browser-dynamic';
|
||||
// #docregion
|
||||
import { bootstrap } from '@angular/platform-browser-dynamic';
|
||||
import { disableDeprecatedForms, provideForms } from '@angular/forms';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
bootstrap(AppComponent, [])
|
||||
.catch((err: any) => console.error(err));
|
||||
bootstrap(AppComponent, [
|
||||
disableDeprecatedForms(),
|
||||
provideForms()
|
||||
])
|
||||
.catch((err: any) => console.error(err));
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
// #docregion
|
||||
import { Injectable } from '@angular/core';
|
||||
import { FormBuilder, Validators } from '@angular/common';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
|
||||
import { QuestionBase } from './question-base';
|
||||
|
||||
@Injectable()
|
||||
export class QuestionControlService {
|
||||
constructor(private fb: FormBuilder) { }
|
||||
constructor() { }
|
||||
|
||||
toControlGroup(questions: QuestionBase<any>[] ) {
|
||||
let group = {};
|
||||
toFormGroup(questions: QuestionBase<any>[] ) {
|
||||
let group: any = {};
|
||||
|
||||
questions.forEach(question => {
|
||||
group[question.key] = question.required ? [question.value || '', Validators.required] : [question.value || ''];
|
||||
group[question.key] = question.required ? new FormControl(question.value || '', Validators.required)
|
||||
: new FormControl(question.value || '');
|
||||
});
|
||||
return this.fb.group(group);
|
||||
return new FormGroup(group);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
// #docregion
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { DropdownQuestion } from './question-dropdown';
|
||||
import { QuestionBase } from './question-base';
|
||||
import { TextboxQuestion } from './question-textbox';
|
||||
import { DropdownQuestion } from './question-dropdown';
|
||||
|
||||
@Injectable()
|
||||
export class QuestionService {
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
"@angular/common": "2.0.0-rc.2",
|
||||
"@angular/compiler": "2.0.0-rc.2",
|
||||
"@angular/core": "2.0.0-rc.2",
|
||||
"@angular/forms": "0.1.0",
|
||||
"@angular/http": "2.0.0-rc.2",
|
||||
"@angular/platform-browser": "2.0.0-rc.2",
|
||||
"@angular/platform-browser-dynamic": "2.0.0-rc.2",
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
'common',
|
||||
'compiler',
|
||||
'core',
|
||||
'forms',
|
||||
'http',
|
||||
'platform-browser',
|
||||
'platform-browser-dynamic',
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
var ngVer = '@2.0.0-rc.2'; // lock in the angular package version; do not let it float to current!
|
||||
var routerVer = '@3.0.0-alpha.7'; // lock router version
|
||||
var formsVer = '@0.1.0'; // lock forms version
|
||||
|
||||
//map tells the System loader where to look for things
|
||||
var map = {
|
||||
|
@ -14,6 +15,7 @@
|
|||
|
||||
'@angular': 'https://npmcdn.com/@angular', // sufficient if we didn't pin the version
|
||||
'@angular/router': 'https://npmcdn.com/@angular/router' + routerVer,
|
||||
'@angular/forms': 'https://npmcdn.com/@angular/forms' + formsVer,
|
||||
'angular2-in-memory-web-api': 'https://npmcdn.com/angular2-in-memory-web-api', // get latest
|
||||
'rxjs': 'https://npmcdn.com/rxjs@5.0.0-beta.6',
|
||||
'ts': 'https://npmcdn.com/plugin-typescript@4.0.10/lib/plugin.js',
|
||||
|
@ -57,6 +59,9 @@
|
|||
// No umd for router yet
|
||||
packages['@angular/router'] = { main: 'index.js', defaultExtension: 'js' };
|
||||
|
||||
// Forms not on rc yet
|
||||
packages['@angular/forms'] = { main: 'index.js', defaultExtension: 'js' };
|
||||
|
||||
var config = {
|
||||
// DEMO ONLY! REAL CODE SHOULD NOT TRANSPILE IN THE BROWSER
|
||||
transpiler: 'ts',
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
"intro": "Techniques for Dependency Injection"
|
||||
},
|
||||
|
||||
"dynamic-form": {
|
||||
"dynamic-form-deprecated": {
|
||||
"title": "Dynamic Form",
|
||||
"intro": "Render dynamic forms with NgFormModel"
|
||||
},
|
||||
|
|
|
@ -0,0 +1,149 @@
|
|||
include ../_util-fns
|
||||
|
||||
.alert.is-important
|
||||
:marked
|
||||
This cookbook is using the deprecated forms API.
|
||||
|
||||
We have created a new version of this cookbook using the new API <a href='/docs/ts/latest/cookbook/dynamic-form.html'>here</a>.
|
||||
|
||||
: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 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.
|
||||
|
||||
In 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*.
|
||||
|
||||
<a id="toc"></a>
|
||||
: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-deprecated/ts/plnkr.html)**.
|
||||
|
||||
.l-main-section
|
||||
<a id="object-model"></a>
|
||||
: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-deprecated/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-deprecated/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-deprecated/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-deprecated/ts/app/question-control.service.ts',null,'app/question-control.service.ts')(format='.')
|
||||
|
||||
<a id="form-component"></a>
|
||||
:marked
|
||||
## Question form components
|
||||
Now that we have defined the complete model we are ready to create components to represent the dynamic form.
|
||||
|
||||
:marked
|
||||
`DynamicFormComponent` is the entry point and the main container for the form.
|
||||
+makeTabs(
|
||||
`cb-dynamic-form-deprecated/ts/app/dynamic-form.component.html,
|
||||
cb-dynamic-form-deprecated/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 `<df-question>` component element.
|
||||
The `<df-question>` 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-deprecated/ts/app/dynamic-form-question.component.html,
|
||||
cb-dynamic-form-deprecated/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 **ngFormModel** to connect the template HTML to the
|
||||
underlying control objects, populated from the question model with display and validation rules.
|
||||
|
||||
<a id="questionnaire-metadata"></a>
|
||||
:marked
|
||||
## Questionnaire data
|
||||
:marked
|
||||
`DynamicFormComponent` 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-deprecated/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-deprecated/ts/app/app.component.ts','','app.component.ts')
|
||||
|
||||
<a id="dynamic-template"></a>
|
||||
:marked
|
||||
## Dynamic Template
|
||||
Although in this example we're modelling 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)
|
|
@ -1,5 +1,11 @@
|
|||
include ../_util-fns
|
||||
|
||||
.alert.is-important
|
||||
:marked
|
||||
This cookbook uses the new forms API.
|
||||
|
||||
The old forms API is deprecated, but we still maintain a separate version of the cookbook using the deprecated forms API <a href='/docs/ts/latest/cookbook/dynamic-form-deprecated.html'>here</a>.
|
||||
|
||||
: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
|
||||
|
@ -7,7 +13,7 @@ include ../_util-fns
|
|||
|
||||
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 render a simple form with different control types and validation.
|
||||
In this cookbook we show how to use `formGroup` to dynamically 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.
|
||||
|
@ -20,6 +26,8 @@ include ../_util-fns
|
|||
:marked
|
||||
## Table of contents
|
||||
|
||||
[Bootstrap](#bootstrap)
|
||||
|
||||
[Question Model](#object-model)
|
||||
|
||||
[Form Component](#form-component)
|
||||
|
@ -31,12 +39,28 @@ include ../_util-fns
|
|||
:marked
|
||||
**See the [live example](/resources/live-examples/cb-dynamic-form/ts/plnkr.html)**.
|
||||
|
||||
.l-main-section
|
||||
<a id="bootstrap"></a>
|
||||
:marked
|
||||
## Bootstrap
|
||||
|
||||
During bootstrap we have to register the new forms module by calling `provideForms()` and pass the result to the provider array.
|
||||
|
||||
+makeExample('cb-dynamic-form/ts/app/main.ts','','app/main.ts')
|
||||
|
||||
:marked
|
||||
The old forms API is going through a deprecation phase. During this transition Angular is supporting both form modules.
|
||||
|
||||
To remind us that the old API is deprecated, Angular will print a warning message to the console.
|
||||
|
||||
Since we are converting to the new API, and no longer need the old API, we call `disableDeprecatedForms()` to disable the old form functionality and the warning message.
|
||||
|
||||
.l-main-section
|
||||
<a id="object-model"></a>
|
||||
:marked
|
||||
## Question Model
|
||||
|
||||
The first step is to define an object model that can describe all scenarios needed by the form functionality.
|
||||
The next 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.
|
||||
|
||||
|
@ -58,8 +82,8 @@ include ../_util-fns
|
|||
+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.
|
||||
Next we have defined `QuestionControlService`, a simple service for transforming our questions to a `FormGroup`.
|
||||
In a nutshell, the form 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='.')
|
||||
|
||||
|
@ -94,9 +118,13 @@ include ../_util-fns
|
|||
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 **ngFormModel** to connect the template HTML to the
|
||||
In both components we're relying on Angular's **formGroup** to connect the template HTML to the
|
||||
underlying control objects, populated from the question model with display and validation rules.
|
||||
|
||||
`formControlName` and `formGroup` have to be registered as directives before we can use them in our templates.
|
||||
|
||||
It turns out we get access to all form directives by importing and registering `REACTIVE_FORM_DIRECTIVES`.
|
||||
|
||||
<a id="questionnaire-metadata"></a>
|
||||
:marked
|
||||
## Questionnaire data
|
||||
|
|
Loading…
Reference in New Issue