diff --git a/.travis.yml b/.travis.yml index a8084eae11..48358bc56e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ os: - linux env: global: + - DBUS_SESSION_BUS_ADDRESS=/dev/null - DISPLAY=:99.0 - CHROME_BIN=chromium-browser matrix: diff --git a/gulpfile.js b/gulpfile.js index d2f8eb4c44..b152763449 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -37,7 +37,7 @@ var DOCS_PATH = path.join(PUBLIC_PATH, 'docs'); var EXAMPLES_PATH = path.join(DOCS_PATH, '_examples'); var EXAMPLES_PROTRACTOR_PATH = path.join(EXAMPLES_PATH, '_protractor'); -var NOT_API_DOCS_GLOB = path.join(PUBLIC_PATH, './{docs/*/latest/!(api),!(docs)}/**/*'); +var NOT_API_DOCS_GLOB = path.join(PUBLIC_PATH, './{docs/*/latest/!(api),!(docs)}/**/*.*'); var RESOURCES_PATH = path.join(PUBLIC_PATH, 'resources'); var LIVE_EXAMPLES_PATH = path.join(RESOURCES_PATH, 'live-examples'); @@ -991,6 +991,7 @@ function devGuideExamplesWatch(shredOptions, postShredAction) { // gulp.watch([includePattern, excludePattern], {readDelay: 500}, function (event, done) { var ignoreThese = [ '**/node_modules/**', '**/_fragments/**', '**/dist/**', '**/typings/**', '**/dart/.pub/**', '**/dart/build/**', '**/dart/packages/**']; + ignoreThese = ignoreThese.concat(_exampleBoilerplateFiles.map((file) => `public/docs/_examples/*/*/${file}`)); var files = globby.sync( [includePattern], { ignore: ignoreThese }); gulp.watch([files], {readDelay: 500}, function (event, done) { gutil.log('Dev Guide example changed') @@ -1005,7 +1006,8 @@ function devGuideSharedJadeWatch(shredOptions, postShredAction) { // removed this version because gulp.watch has the same glob issue that dgeni has. // var excludePattern = '!' + path.join(shredOptions.jadeDir, '**/node_modules/**/*.*'); // gulp.watch([includePattern, excludePattern], {readDelay: 500}, function (event, done) { - var files = globby.sync( [includePattern], { ignore: [ '**/node_modules/**', '**/_examples/**', '**/_fragments/**']}); + var ignoreThese = [ '**/node_modules/**', '**/_examples/**', '**/_fragments/**', '**/latest/api/**' ]; + var files = globby.sync( [includePattern], { ignore: ignoreThese}); gulp.watch([files], {readDelay: 500}, function (event, done) { gutil.log('Dev Guide jade file changed') gutil.log('Event type: ' + event.type); // added, changed, or deleted diff --git a/harp.json b/harp.json index e79fb92241..0d9b1cb9f8 100644 --- a/harp.json +++ b/harp.json @@ -88,14 +88,6 @@ "type": "Google" }, - "brianford": { - "name": "Brian Ford", - "picture": "/resources/images/bios/brian-ford.jpg", - "twitter": "briantford", - "bio": "Brian works on the AngularJS core team at Google where he tries his very best to make computers do the right thing.", - "type": "Google" - }, - "rado": { "name": "Rado Kirov", "picture": "/resources/images/bios/rado.jpg", @@ -294,6 +286,15 @@ "bio": "Stephen is a Developer Advocate working on the Angular team. Before joining Google, he was a Google Expert. Stephen loves to help enterprises use technology more effectively.", "type": "Google" }, + "robwormald": { + "name": "Rob Wormald", + "picture": "/resources/images/bios/rob-wormald.jpg", + "twitter": "robwormald", + "website": "http://github.com/robwormald", + "bio": "Rob is a Developer Advocate on the Angular team at Google. He's the Angular team's resident reactive programming geek and founded the Reactive Extensions for Angular project, ngrx.", + "type": "Google" + }, + "pawel": { "name": "Pawel Kozlowski", "picture": "/resources/images/bios/pawel.jpg", @@ -422,6 +423,30 @@ "website": "http://angular-tips.com", "bio": "Jesus is an open source lover, a book author and editor, and AngularUI lead developer. He is currently a core contributor to the UI Bootstrap project.", "type": "Community" + }, + "torgeirhelgevold": { + "name": "Torgeir Helgevold", + "picture": "/resources/images/bios/torgeirhelgevold.jpg", + "twitter": "helgevold", + "website": "http://www.syntaxsuccess.com", + "bio": "Torgeir (Tor) is a front-end architect with a passion for JavaScript development. He is also an author for angular.io and an active tech blogger.", + "type": "Community" + }, + "fatimaremtullah": { + "name": "Fatima Remtullah", + "picture": "/resources/images/bios/fatima.jpg", + "twitter": "amitafr", + "website": "http://www.amitafremtullah.com", + "bio": "Fatima is a Product Designer and Front-End Developer. When she is not nerding out she is probably eating an abundance of cookies.", + "type": "Community" + }, + "eric": { + "name": "Eric Jimenez", + "picture": "/resources/images/bios/eric.jpg", + "twitter": "_ericjim", + "website": "http://eric.to/", + "bio": "Eric is a gamer, writer, and programmer.", + "type": "Community" } } } diff --git a/public/docs/_examples/_protractor/typings.json b/public/docs/_examples/_protractor/typings.json index 736747f5a9..a8a5da9134 100644 --- a/public/docs/_examples/_protractor/typings.json +++ b/public/docs/_examples/_protractor/typings.json @@ -1,8 +1,8 @@ { "globalDependencies": { "angular-protractor": "registry:dt/angular-protractor#1.5.0+20160425143459", - "jasmine": "registry:dt/jasmine#2.2.0+20160505161446", - "node": "registry:dt/node#6.0.0+20160613154055", + "jasmine": "registry:dt/jasmine#2.2.0+20160621224255", + "node": "registry:dt/node#6.0.0+20160621231320", "selenium-webdriver": "registry:dt/selenium-webdriver#2.44.0+20160317120654" } } diff --git a/public/docs/_examples/cb-dynamic-form-deprecated/e2e-spec.ts b/public/docs/_examples/cb-dynamic-form-deprecated/e2e-spec.ts new file mode 100644 index 0000000000..725022d0f1 --- /dev/null +++ b/public/docs/_examples/cb-dynamic-form-deprecated/e2e-spec.ts @@ -0,0 +1,27 @@ +/// +'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); + }); + }); + +}); diff --git a/public/docs/_examples/cb-dynamic-form-deprecated/ts/app/app.component.ts b/public/docs/_examples/cb-dynamic-form-deprecated/ts/app/app.component.ts new file mode 100644 index 0000000000..e51561770e --- /dev/null +++ b/public/docs/_examples/cb-dynamic-form-deprecated/ts/app/app.component.ts @@ -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: ` +
+

Job Application for Heroes

+ +
+ `, + directives: [DynamicFormComponent], + providers: [QuestionService] +}) +export class AppComponent { + questions: any[]; + + constructor(service: QuestionService) { + this.questions = service.getQuestions(); + } +} diff --git a/public/docs/_examples/cb-dynamic-form-deprecated/ts/app/dynamic-form-question.component.html b/public/docs/_examples/cb-dynamic-form-deprecated/ts/app/dynamic-form-question.component.html new file mode 100644 index 0000000000..ceb2f41177 --- /dev/null +++ b/public/docs/_examples/cb-dynamic-form-deprecated/ts/app/dynamic-form-question.component.html @@ -0,0 +1,17 @@ + +
+ + +
+ + + + + +
+ +
{{question.label}} is required
+
diff --git a/public/docs/_examples/cb-dynamic-form-deprecated/ts/app/dynamic-form-question.component.ts b/public/docs/_examples/cb-dynamic-form-deprecated/ts/app/dynamic-form-question.component.ts new file mode 100644 index 0000000000..0593fd523e --- /dev/null +++ b/public/docs/_examples/cb-dynamic-form-deprecated/ts/app/dynamic-form-question.component.ts @@ -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; + @Input() form: ControlGroup; + get isValid() { return this.form.controls[this.question.key].valid; } +} diff --git a/public/docs/_examples/cb-dynamic-form-deprecated/ts/app/dynamic-form.component.html b/public/docs/_examples/cb-dynamic-form-deprecated/ts/app/dynamic-form.component.html new file mode 100644 index 0000000000..e8f14612c9 --- /dev/null +++ b/public/docs/_examples/cb-dynamic-form-deprecated/ts/app/dynamic-form.component.html @@ -0,0 +1,17 @@ + +
+
+ +
+ +
+ +
+ +
+
+ +
+ Saved the following values
{{payLoad}} +
+
diff --git a/public/docs/_examples/cb-dynamic-form-deprecated/ts/app/dynamic-form.component.ts b/public/docs/_examples/cb-dynamic-form-deprecated/ts/app/dynamic-form.component.ts new file mode 100644 index 0000000000..5d6eb833ed --- /dev/null +++ b/public/docs/_examples/cb-dynamic-form-deprecated/ts/app/dynamic-form.component.ts @@ -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[] = []; + 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-deprecated/ts/app/main.ts b/public/docs/_examples/cb-dynamic-form-deprecated/ts/app/main.ts new file mode 100644 index 0000000000..5a8d9c5044 --- /dev/null +++ b/public/docs/_examples/cb-dynamic-form-deprecated/ts/app/main.ts @@ -0,0 +1,6 @@ +import { bootstrap } from '@angular/platform-browser-dynamic'; + +import { AppComponent } from './app.component'; + +bootstrap(AppComponent, []) + .catch((err: any) => console.error(err)); diff --git a/public/docs/_examples/cb-dynamic-form-deprecated/ts/app/question-base.ts b/public/docs/_examples/cb-dynamic-form-deprecated/ts/app/question-base.ts new file mode 100644 index 0000000000..2b32b00f2a --- /dev/null +++ b/public/docs/_examples/cb-dynamic-form-deprecated/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-deprecated/ts/app/question-control.service.ts b/public/docs/_examples/cb-dynamic-form-deprecated/ts/app/question-control.service.ts new file mode 100644 index 0000000000..3d56b0c2f6 --- /dev/null +++ b/public/docs/_examples/cb-dynamic-form-deprecated/ts/app/question-control.service.ts @@ -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[] ) { + let group = {}; + + questions.forEach(question => { + group[question.key] = question.required ? [question.value || '', Validators.required] : [question.value || '']; + }); + return this.fb.group(group); + } +} diff --git a/public/docs/_examples/cb-dynamic-form-deprecated/ts/app/question-dropdown.ts b/public/docs/_examples/cb-dynamic-form-deprecated/ts/app/question-dropdown.ts new file mode 100644 index 0000000000..35a9074c74 --- /dev/null +++ b/public/docs/_examples/cb-dynamic-form-deprecated/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-deprecated/ts/app/question-textbox.ts b/public/docs/_examples/cb-dynamic-form-deprecated/ts/app/question-textbox.ts new file mode 100644 index 0000000000..aaa7edf267 --- /dev/null +++ b/public/docs/_examples/cb-dynamic-form-deprecated/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-deprecated/ts/app/question.service.ts b/public/docs/_examples/cb-dynamic-form-deprecated/ts/app/question.service.ts new file mode 100644 index 0000000000..ee169d0827 --- /dev/null +++ b/public/docs/_examples/cb-dynamic-form-deprecated/ts/app/question.service.ts @@ -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[] = [ + + 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-deprecated/ts/example-config.json b/public/docs/_examples/cb-dynamic-form-deprecated/ts/example-config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/public/docs/_examples/cb-dynamic-form-deprecated/ts/index.html b/public/docs/_examples/cb-dynamic-form-deprecated/ts/index.html new file mode 100644 index 0000000000..ec3e1cbbb2 --- /dev/null +++ b/public/docs/_examples/cb-dynamic-form-deprecated/ts/index.html @@ -0,0 +1,29 @@ + + + + + + Dynamic Form + + + + + + + + + + + + + + + + + + Loading app... + + + diff --git a/public/docs/_examples/cb-dynamic-form-deprecated/ts/plnkr.json b/public/docs/_examples/cb-dynamic-form-deprecated/ts/plnkr.json new file mode 100644 index 0000000000..5034d652aa --- /dev/null +++ b/public/docs/_examples/cb-dynamic-form-deprecated/ts/plnkr.json @@ -0,0 +1,9 @@ +{ + "description": "Dynamic Form Deprecated", + "files":[ + "!**/*.d.ts", + "!**/*.js", + "!**/*.[1].*" + ], + "tags":["cookbook"] +} diff --git a/public/docs/_examples/cb-dynamic-form-deprecated/ts/sample.css b/public/docs/_examples/cb-dynamic-form-deprecated/ts/sample.css new file mode 100644 index 0000000000..fe2cc28481 --- /dev/null +++ b/public/docs/_examples/cb-dynamic-form-deprecated/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/_examples/cb-dynamic-form/ts/app/dynamic-form-question.component.html b/public/docs/_examples/cb-dynamic-form/ts/app/dynamic-form-question.component.html index ceb2f41177..9f1b8cd4a6 100644 --- a/public/docs/_examples/cb-dynamic-form/ts/app/dynamic-form-question.component.html +++ b/public/docs/_examples/cb-dynamic-form/ts/app/dynamic-form-question.component.html @@ -1,17 +1,17 @@ -
+
- - -
+
{{question.label}} is required
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 index 0593fd523e..3381d1c444 100644 --- 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 @@ -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; - @Input() form: ControlGroup; + @Input() form: FormGroup; 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 index e8f14612c9..717f09ff71 100644 --- 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 @@ -1,6 +1,6 @@
-
+
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 index 5d6eb833ed..15730806d1 100644 --- 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 @@ -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[] = []; - 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() { diff --git a/public/docs/_examples/cb-dynamic-form/ts/app/main.ts b/public/docs/_examples/cb-dynamic-form/ts/app/main.ts index 5a8d9c5044..461ce210c6 100644 --- a/public/docs/_examples/cb-dynamic-form/ts/app/main.ts +++ b/public/docs/_examples/cb-dynamic-form/ts/app/main.ts @@ -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)); 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 index 3d56b0c2f6..1378ba8490 100644 --- 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 @@ -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[] ) { - let group = {}; + toFormGroup(questions: QuestionBase[] ) { + 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); } } 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 index ee169d0827..bb452cf5e6 100644 --- a/public/docs/_examples/cb-dynamic-form/ts/app/question.service.ts +++ b/public/docs/_examples/cb-dynamic-form/ts/app/question.service.ts @@ -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 { diff --git a/public/docs/_examples/forms-deprecated/e2e-spec.ts b/public/docs/_examples/forms-deprecated/e2e-spec.ts new file mode 100644 index 0000000000..ab5c8321be --- /dev/null +++ b/public/docs/_examples/forms-deprecated/e2e-spec.ts @@ -0,0 +1,64 @@ +/// +'use strict'; +describeIf(browser.appIsTs || browser.appIsJs, 'Forms (Deprecated) Tests', function () { + + beforeEach(function () { + browser.get(''); + }); + + it('should display correct title', function () { + expect(element.all(by.css('h1')).get(0).getText()).toEqual('Hero Form'); + }); + + + it('should not display message before submit', function () { + let ele = element(by.css('h2')); + expect(ele.isDisplayed()).toBe(false); + }); + + it('should hide form after submit', function () { + let ele = element.all(by.css('h1')).get(0); + expect(ele.isDisplayed()).toBe(true); + let b = element.all(by.css('button[type=submit]')).get(0); + b.click().then(function() { + expect(ele.isDisplayed()).toBe(false); + }); + }); + + it('should display message after submit', function () { + let b = element.all(by.css('button[type=submit]')).get(0); + b.click().then(function() { + expect(element(by.css('h2')).getText()).toContain('You submitted the following'); + }); + }); + + it('should hide form after submit', function () { + let alterEgoEle = element.all(by.css('input[ngcontrol=alterEgo]')).get(0); + expect(alterEgoEle.isDisplayed()).toBe(true); + let submitButtonEle = element.all(by.css('button[type=submit]')).get(0); + submitButtonEle.click().then(function() { + expect(alterEgoEle.isDisplayed()).toBe(false); + }); + }); + + it('should reflect submitted data after submit', function () { + let test = 'testing 1 2 3'; + let newValue: string; + let alterEgoEle = element.all(by.css('input[ngcontrol=alterEgo]')).get(0); + alterEgoEle.getAttribute('value').then(function(value) { + // alterEgoEle.sendKeys(test); + sendKeys(alterEgoEle, test); + newValue = value + test; + expect(alterEgoEle.getAttribute('value')).toEqual(newValue); + }).then(function() { + let b = element.all(by.css('button[type=submit]')).get(0); + return b.click(); + }).then(function() { + let alterEgoTextEle = element(by.cssContainingText('div', 'Alter Ego')); + expect(alterEgoTextEle.isPresent()).toBe(true, 'cannot locate "Alter Ego" label'); + let divEle = element(by.cssContainingText('div', newValue)); + expect(divEle.isPresent()).toBe(true, 'cannot locate div with this text: ' + newValue); + }); + }); +}); + diff --git a/public/docs/_examples/forms-deprecated/js/app/app.component.js b/public/docs/_examples/forms-deprecated/js/app/app.component.js new file mode 100644 index 0000000000..bb6b789938 --- /dev/null +++ b/public/docs/_examples/forms-deprecated/js/app/app.component.js @@ -0,0 +1,12 @@ +// #docregion +(function(app) { + app.AppComponent = ng.core + .Component({ + selector: 'my-app', + template: '', + directives: [app.HeroFormComponent] + }) + .Class({ + constructor: function() {} + }); +})(window.app || (window.app = {})); diff --git a/public/docs/_examples/forms-deprecated/js/app/hero-form.component.html b/public/docs/_examples/forms-deprecated/js/app/hero-form.component.html new file mode 100644 index 0000000000..fdf8e08396 --- /dev/null +++ b/public/docs/_examples/forms-deprecated/js/app/hero-form.component.html @@ -0,0 +1,195 @@ + + +
+ +
+

Hero Form

+ + + + +
+ + + +
+ Name is required +
+ +
+ +
+ + +
+ +
+ + +
+ Power is required +
+
+ + + + + +
+ + +
+

You submitted the following:

+
+
Name
+
{{ model.name }}
+
+
+
Alter Ego
+
{{ model.alterEgo }}
+
+
+
Power
+
{{ model.power }}
+
+
+ +
+ +
+ + + +
+
+ + + + +
+
+ + + +
+ +
+ +
+

Hero Form

+
+
+ + +
+ +
+ + +
+ + + +
+ + +
+ + + + +
+
+ + + + +
+ +
+

Hero Form

+
+ + {{diagnostic()}} +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + +
+
+ + + +
+ + + TODO: remove this: {{model.name}} + +
+ + + TODO: remove this: {{model.name}} + +
+
+ + + +
+ + +
TODO: remove this: {{spy.className}} + +
+ +
+
+ Name via form.controls = {{showFormControls(heroForm)}} +
+ +
diff --git a/public/docs/_examples/forms-deprecated/js/app/hero-form.component.js b/public/docs/_examples/forms-deprecated/js/app/hero-form.component.js new file mode 100644 index 0000000000..8988231189 --- /dev/null +++ b/public/docs/_examples/forms-deprecated/js/app/hero-form.component.js @@ -0,0 +1,52 @@ +// #docplaster +// #docregion +// #docregion first, final +(function(app) { + app.HeroFormComponent = ng.core + .Component({ + selector: 'hero-form', + templateUrl: 'app/hero-form.component.html' + }) + .Class({ + // #docregion submitted + constructor: function() { + // #enddocregion submitted + this.powers = ['Really Smart', 'Super Flexible', + 'Super Hot', 'Weather Changer' + ]; + + this.model = new app.Hero(18, 'Dr IQ', this.powers[0], + 'Chuck Overstreet'); + + // #docregion submitted + this.submitted = false; + }, + onSubmit: function() { + this.submitted = true; + }, + // #enddocregion submitted + + // #enddocregion final + // TODO: Remove this when we're done + diagnostic: function() { + return JSON.stringify(this.model); + }, + // #enddocregion first + + + //////// DO NOT SHOW IN DOCS //////// + + // Reveal in html: + // AlterEgo via form.controls = {{showFormControls(hf)}} + showFormControls: function(form) { + return form.controls['alterEgo'] && + // #docregion form-controls + form.controls['name'].value; // Dr. IQ + // #enddocregion form-controls + }, + ///////////////////////////// + + // #docregion first, final + }); + // #enddocregion first, final +})(window.app || (window.app = {})); diff --git a/public/docs/_examples/forms-deprecated/js/app/hero.js b/public/docs/_examples/forms-deprecated/js/app/hero.js new file mode 100644 index 0000000000..9c2449c922 --- /dev/null +++ b/public/docs/_examples/forms-deprecated/js/app/hero.js @@ -0,0 +1,11 @@ +// #docregion +(function(app) { + app.Hero = Hero; + + function Hero(id, name, power, alterEgo) { + this.id = id; + this.name = name; + this.power = power; + this.alterEgo = alterEgo; + } +})(window.app || (window.app = {})); diff --git a/public/docs/_examples/forms-deprecated/js/app/main.js b/public/docs/_examples/forms-deprecated/js/app/main.js new file mode 100644 index 0000000000..5930bdd061 --- /dev/null +++ b/public/docs/_examples/forms-deprecated/js/app/main.js @@ -0,0 +1,6 @@ +// #docregion +(function(app) { + document.addEventListener('DOMContentLoaded', function() { + ng.platformBrowserDynamic.bootstrap(app.AppComponent); + }); +})(window.app || (window.app = {})); diff --git a/public/docs/_examples/forms-deprecated/js/example-config.json b/public/docs/_examples/forms-deprecated/js/example-config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/public/docs/_examples/forms-deprecated/js/forms.css b/public/docs/_examples/forms-deprecated/js/forms.css new file mode 100644 index 0000000000..d7e11405b1 --- /dev/null +++ b/public/docs/_examples/forms-deprecated/js/forms.css @@ -0,0 +1,9 @@ +/* #docregion */ +.ng-valid[required] { + border-left: 5px solid #42A948; /* green */ +} + +.ng-invalid { + border-left: 5px solid #a94442; /* red */ +} +/* #enddocregion */ \ No newline at end of file diff --git a/public/docs/_examples/forms-deprecated/js/index.html b/public/docs/_examples/forms-deprecated/js/index.html new file mode 100644 index 0000000000..e15f41e4fe --- /dev/null +++ b/public/docs/_examples/forms-deprecated/js/index.html @@ -0,0 +1,45 @@ + + + + + + Hero Form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Loading... + + + diff --git a/public/docs/_examples/forms-deprecated/js/plnkr.json b/public/docs/_examples/forms-deprecated/js/plnkr.json new file mode 100644 index 0000000000..0105283bd3 --- /dev/null +++ b/public/docs/_examples/forms-deprecated/js/plnkr.json @@ -0,0 +1,4 @@ +{ + "description": "Forms", + "files":["app/**/*.js", "**/*.html", "**/*.css"] +} diff --git a/public/docs/_examples/forms-deprecated/ts/app/app.component.ts b/public/docs/_examples/forms-deprecated/ts/app/app.component.ts new file mode 100644 index 0000000000..2dcbc8037a --- /dev/null +++ b/public/docs/_examples/forms-deprecated/ts/app/app.component.ts @@ -0,0 +1,10 @@ +// #docregion +import { Component } from '@angular/core'; +import { HeroFormComponent } from './hero-form.component'; + +@Component({ + selector: 'my-app', + template: '', + directives: [HeroFormComponent] +}) +export class AppComponent { } diff --git a/public/docs/_examples/forms-deprecated/ts/app/hero-form.component.html b/public/docs/_examples/forms-deprecated/ts/app/hero-form.component.html new file mode 100644 index 0000000000..6fcfc2a7cb --- /dev/null +++ b/public/docs/_examples/forms-deprecated/ts/app/hero-form.component.html @@ -0,0 +1,208 @@ + + +
+ +
+

Hero Form

+ +
+ + +
+ + + + +
+ + Name is required +
+ +
+ +
+ + +
+ +
+ + +
+ Power is required +
+
+ + + + + + + + + + + +
+
+ Name via form.controls = {{showFormControls(heroForm)}} +
+ + +
+
+ + +
+

You submitted the following:

+
+
Name
+
{{ model.name }}
+
+
+
Alter Ego
+
{{ model.alterEgo }}
+
+
+
Power
+
{{ model.power }}
+
+
+ +
+ +
+ + + +
+
+ + + + +
+
+ + + +
+ +
+ +
+

Hero Form

+
+
+ + +
+ +
+ + +
+ + + +
+ + +
+ + + + + +
+
+ + + + +
+ +
+

Hero Form

+
+ + {{diagnostic}} +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + +
+
+ + + +
+ + + TODO: remove this: {{model.name}} + +
+ + + TODO: remove this: {{model.name}} + +
+ +
+ + + + + +
+ + +
TODO: remove this: {{spy.className}} + +
+ +
diff --git a/public/docs/_examples/forms-deprecated/ts/app/hero-form.component.ts b/public/docs/_examples/forms-deprecated/ts/app/hero-form.component.ts new file mode 100644 index 0000000000..7ea7c44738 --- /dev/null +++ b/public/docs/_examples/forms-deprecated/ts/app/hero-form.component.ts @@ -0,0 +1,66 @@ +// #docplaster +// #docregion +// #docregion first, final +import { Component } from '@angular/core'; +import { NgForm } from '@angular/common'; + +import { Hero } from './hero'; + +@Component({ + selector: 'hero-form', + templateUrl: 'app/hero-form.component.html' +}) +export class HeroFormComponent { + + powers = ['Really Smart', 'Super Flexible', + 'Super Hot', 'Weather Changer']; + + model = new Hero(18, 'Dr IQ', this.powers[0], 'Chuck Overstreet'); + + // #docregion submitted + submitted = false; + + onSubmit() { this.submitted = true; } + // #enddocregion submitted + + // #enddocregion final + // TODO: Remove this when we're done + get diagnostic() { return JSON.stringify(this.model); } + // #enddocregion first + + // #docregion final + // Reset the form with a new hero AND restore 'pristine' class state + // by toggling 'active' flag which causes the form + // to be removed/re-added in a tick via NgIf + // TODO: Workaround until NgForm has a reset method (#6822) + // #docregion new-hero + active = true; + + // #docregion new-hero-v1 + newHero() { + this.model = new Hero(42, '', ''); + // #enddocregion new-hero-v1 + this.active = false; + setTimeout(() => this.active = true, 0); + // #docregion new-hero-v1 + } + // #enddocregion new-hero-v1 + // #enddocregion new-hero + // #enddocregion final + //////// NOT SHOWN IN DOCS //////// + + // Reveal in html: + // Name via form.controls = {{showFormControls(heroForm)}} + showFormControls(form: NgForm) { + + return form && form.controls['name'] && + // #docregion form-controls + form.controls['name'].value; // Dr. IQ + // #enddocregion form-controls + } + + ///////////////////////////// + + // #docregion first, final +} +// #enddocregion first, final diff --git a/public/docs/_examples/forms-deprecated/ts/app/hero.ts b/public/docs/_examples/forms-deprecated/ts/app/hero.ts new file mode 100644 index 0000000000..c128626452 --- /dev/null +++ b/public/docs/_examples/forms-deprecated/ts/app/hero.ts @@ -0,0 +1,11 @@ +// #docregion +export class Hero { + + constructor( + public id: number, + public name: string, + public power: string, + public alterEgo?: string + ) { } + +} diff --git a/public/docs/_examples/forms-deprecated/ts/app/main.ts b/public/docs/_examples/forms-deprecated/ts/app/main.ts new file mode 100644 index 0000000000..5338161d66 --- /dev/null +++ b/public/docs/_examples/forms-deprecated/ts/app/main.ts @@ -0,0 +1,6 @@ +// #docregion +import { bootstrap } from '@angular/platform-browser-dynamic'; + +import { AppComponent } from './app.component'; + +bootstrap(AppComponent); diff --git a/public/docs/_examples/forms-deprecated/ts/example-config.json b/public/docs/_examples/forms-deprecated/ts/example-config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/public/docs/_examples/forms-deprecated/ts/forms.css b/public/docs/_examples/forms-deprecated/ts/forms.css new file mode 100644 index 0000000000..d7e11405b1 --- /dev/null +++ b/public/docs/_examples/forms-deprecated/ts/forms.css @@ -0,0 +1,9 @@ +/* #docregion */ +.ng-valid[required] { + border-left: 5px solid #42A948; /* green */ +} + +.ng-invalid { + border-left: 5px solid #a94442; /* red */ +} +/* #enddocregion */ \ No newline at end of file diff --git a/public/docs/_examples/forms-deprecated/ts/index.html b/public/docs/_examples/forms-deprecated/ts/index.html new file mode 100644 index 0000000000..4df2d32d46 --- /dev/null +++ b/public/docs/_examples/forms-deprecated/ts/index.html @@ -0,0 +1,34 @@ + + + + + Hero Form + + + + + + + + + + + + + + + + + + + + + + + + Loading... + + + diff --git a/public/docs/_examples/forms-deprecated/ts/plnkr.json b/public/docs/_examples/forms-deprecated/ts/plnkr.json new file mode 100644 index 0000000000..c813933f8e --- /dev/null +++ b/public/docs/_examples/forms-deprecated/ts/plnkr.json @@ -0,0 +1,7 @@ +{ + "description": "Forms-Deprecated", + "files":[ + "!**/*.d.ts", + "!**/*.js" + ] +} \ No newline at end of file diff --git a/public/docs/_examples/forms/e2e-spec.ts b/public/docs/_examples/forms/e2e-spec.ts index 502a2cbdce..87ab6ef904 100644 --- a/public/docs/_examples/forms/e2e-spec.ts +++ b/public/docs/_examples/forms/e2e-spec.ts @@ -33,7 +33,7 @@ describeIf(browser.appIsTs || browser.appIsJs, 'Forms Tests', function () { }); it('should hide form after submit', function () { - let alterEgoEle = element.all(by.css('input[ngcontrol=alterEgo]')).get(0); + let alterEgoEle = element.all(by.css('input[name=alterEgo]')).get(0); expect(alterEgoEle.isDisplayed()).toBe(true); let submitButtonEle = element.all(by.css('button[type=submit]')).get(0); submitButtonEle.click().then(function() { @@ -44,7 +44,7 @@ describeIf(browser.appIsTs || browser.appIsJs, 'Forms Tests', function () { it('should reflect submitted data after submit', function () { let test = 'testing 1 2 3'; let newValue: string; - let alterEgoEle = element.all(by.css('input[ngcontrol=alterEgo]')).get(0); + let alterEgoEle = element.all(by.css('input[name=alterEgo]')).get(0); alterEgoEle.getAttribute('value').then(function(value) { // alterEgoEle.sendKeys(test); sendKeys(alterEgoEle, test); diff --git a/public/docs/_examples/forms/js/app/hero-form.component.html b/public/docs/_examples/forms/js/app/hero-form.component.html index fdf8e08396..fbe6ee917f 100644 --- a/public/docs/_examples/forms/js/app/hero-form.component.html +++ b/public/docs/_examples/forms/js/app/hero-form.component.html @@ -13,7 +13,7 @@ + name="name" #name="ngModel" >
Name is required
@@ -24,14 +24,14 @@ + name="alterEgo" >
@@ -133,19 +133,19 @@
+ [(ngModel)]="model.name" name="name" >
+ [(ngModel)]="model.alterEgo" name="alterEgo">
@@ -173,18 +173,18 @@
- + - + name="name" > +
- + + name="name" #spy >
TODO: remove this: {{spy.className}} - +
diff --git a/public/docs/_examples/forms/js/app/main.js b/public/docs/_examples/forms/js/app/main.js index 5930bdd061..2b866b3685 100644 --- a/public/docs/_examples/forms/js/app/main.js +++ b/public/docs/_examples/forms/js/app/main.js @@ -1,6 +1,9 @@ // #docregion (function(app) { document.addEventListener('DOMContentLoaded', function() { - ng.platformBrowserDynamic.bootstrap(app.AppComponent); + ng.platformBrowserDynamic.bootstrap(app.AppComponent,[ + ng.forms.disableDeprecatedForms(), + ng.forms.provideForms() + ]); }); })(window.app || (window.app = {})); diff --git a/public/docs/_examples/forms/js/index.html b/public/docs/_examples/forms/js/index.html index e15f41e4fe..8353dfb180 100644 --- a/public/docs/_examples/forms/js/index.html +++ b/public/docs/_examples/forms/js/index.html @@ -24,6 +24,7 @@ + diff --git a/public/docs/_examples/forms/ts/app/hero-form.component.html b/public/docs/_examples/forms/ts/app/hero-form.component.html index 6fcfc2a7cb..80401a1313 100644 --- a/public/docs/_examples/forms/ts/app/hero-form.component.html +++ b/public/docs/_examples/forms/ts/app/hero-form.component.html @@ -13,7 +13,7 @@ + name="name" #name="ngModel" >
@@ -26,14 +26,14 @@ + name="alterEgo" >
@@ -148,19 +148,19 @@
+ [(ngModel)]="model.name" name="name">
+ [(ngModel)]="model.alterEgo" name="alterEgo">
@@ -176,7 +176,7 @@
+ [(ngModel)]="model.name" name="name"> TODO: remove this: {{model.name}}
@@ -191,18 +191,18 @@
- + - + name="name" > +
- + + name="name" #spy >
TODO: remove this: {{spy.className}} - +
diff --git a/public/docs/_examples/forms/ts/app/main.ts b/public/docs/_examples/forms/ts/app/main.ts index 5338161d66..9e0244c26a 100644 --- a/public/docs/_examples/forms/ts/app/main.ts +++ b/public/docs/_examples/forms/ts/app/main.ts @@ -1,6 +1,11 @@ // #docregion import { bootstrap } from '@angular/platform-browser-dynamic'; +import { disableDeprecatedForms, provideForms } from '@angular/forms'; import { AppComponent } from './app.component'; -bootstrap(AppComponent); +bootstrap(AppComponent, [ + disableDeprecatedForms(), + provideForms() + ]) + .catch((err: any) => console.error(err)); diff --git a/public/docs/_examples/package.json b/public/docs/_examples/package.json index 1cb56e65de..4f4dd51f00 100644 --- a/public/docs/_examples/package.json +++ b/public/docs/_examples/package.json @@ -25,15 +25,16 @@ "author": "", "license": "ISC", "dependencies": { - "@angular/common": "2.0.0-rc.2", - "@angular/compiler": "2.0.0-rc.2", - "@angular/core": "2.0.0-rc.2", - "@angular/http": "2.0.0-rc.2", - "@angular/platform-browser": "2.0.0-rc.2", - "@angular/platform-browser-dynamic": "2.0.0-rc.2", - "@angular/router": "2.0.0-rc.2", + "@angular/common": "2.0.0-rc.3", + "@angular/compiler": "2.0.0-rc.3", + "@angular/core": "2.0.0-rc.3", + "@angular/forms": "0.1.1", + "@angular/http": "2.0.0-rc.3", + "@angular/platform-browser": "2.0.0-rc.3", + "@angular/platform-browser-dynamic": "2.0.0-rc.3", + "@angular/router": "3.0.0-alpha.7", "@angular/router-deprecated": "2.0.0-rc.2", - "@angular/upgrade": "2.0.0-rc.2", + "@angular/upgrade": "2.0.0-rc.3", "angular2-in-memory-web-api": "0.0.12", "bootstrap": "^3.3.6", "core-js": "^2.4.0", diff --git a/public/docs/_examples/quickstart/js/package.1.json b/public/docs/_examples/quickstart/js/package.1.json index c54e43307a..b5b25a4f10 100644 --- a/public/docs/_examples/quickstart/js/package.1.json +++ b/public/docs/_examples/quickstart/js/package.1.json @@ -7,15 +7,16 @@ }, "license": "ISC", "dependencies": { - "@angular/common": "2.0.0-rc.2", - "@angular/compiler": "2.0.0-rc.2", - "@angular/core": "2.0.0-rc.2", - "@angular/http": "2.0.0-rc.2", - "@angular/platform-browser": "2.0.0-rc.2", - "@angular/platform-browser-dynamic": "2.0.0-rc.2", - "@angular/router": "2.0.0-rc.2", + "@angular/common": "2.0.0-rc.3", + "@angular/compiler": "2.0.0-rc.3", + "@angular/core": "2.0.0-rc.3", + "@angular/forms": "0.1.1", + "@angular/http": "2.0.0-rc.3", + "@angular/platform-browser": "2.0.0-rc.3", + "@angular/platform-browser-dynamic": "2.0.0-rc.3", + "@angular/router": "3.0.0-alpha.7", "@angular/router-deprecated": "2.0.0-rc.2", - "@angular/upgrade": "2.0.0-rc.2", + "@angular/upgrade": "2.0.0-rc.3", "core-js": "^2.4.0", "reflect-metadata": "0.1.3", diff --git a/public/docs/_examples/quickstart/js/styles.1.css b/public/docs/_examples/quickstart/js/styles.1.css new file mode 100644 index 0000000000..fbc30e2c9e --- /dev/null +++ b/public/docs/_examples/quickstart/js/styles.1.css @@ -0,0 +1,14 @@ +/* #docregion */ +h1 { + color: #369; + font-family: Arial, Helvetica, sans-serif; + font-size: 250%; +} +body { + margin: 2em; +} + + /* + * See https://github.com/angular/angular.io/blob/master/public/docs/_examples/styles.css + * for the full set of master styles used by the documentation samples + */ diff --git a/public/docs/_examples/quickstart/ts/package.1.json b/public/docs/_examples/quickstart/ts/package.1.json index afd11cdda5..9353dfaa3e 100644 --- a/public/docs/_examples/quickstart/ts/package.1.json +++ b/public/docs/_examples/quickstart/ts/package.1.json @@ -11,15 +11,16 @@ }, "license": "ISC", "dependencies": { - "@angular/common": "2.0.0-rc.2", - "@angular/compiler": "2.0.0-rc.2", - "@angular/core": "2.0.0-rc.2", - "@angular/http": "2.0.0-rc.2", - "@angular/platform-browser": "2.0.0-rc.2", - "@angular/platform-browser-dynamic": "2.0.0-rc.2", - "@angular/router": "2.0.0-rc.2", + "@angular/common": "2.0.0-rc.3", + "@angular/compiler": "2.0.0-rc.3", + "@angular/core": "2.0.0-rc.3", + "@angular/forms": "0.1.1", + "@angular/http": "2.0.0-rc.3", + "@angular/platform-browser": "2.0.0-rc.3", + "@angular/platform-browser-dynamic": "2.0.0-rc.3", + "@angular/router": "3.0.0-alpha.7", "@angular/router-deprecated": "2.0.0-rc.2", - "@angular/upgrade": "2.0.0-rc.2", + "@angular/upgrade": "2.0.0-rc.3", "systemjs": "0.19.27", "core-js": "^2.4.0", diff --git a/public/docs/_examples/quickstart/ts/systemjs.config.1.js b/public/docs/_examples/quickstart/ts/systemjs.config.1.js index 4dbe1974c2..901b8f01d0 100644 --- a/public/docs/_examples/quickstart/ts/systemjs.config.1.js +++ b/public/docs/_examples/quickstart/ts/systemjs.config.1.js @@ -25,6 +25,7 @@ 'common', 'compiler', 'core', + 'forms', 'http', 'platform-browser', 'platform-browser-dynamic', diff --git a/public/docs/_examples/quickstart/ts/typings.1.json b/public/docs/_examples/quickstart/ts/typings.1.json index 9125b50685..3385926d1f 100644 --- a/public/docs/_examples/quickstart/ts/typings.1.json +++ b/public/docs/_examples/quickstart/ts/typings.1.json @@ -1,7 +1,7 @@ { "globalDependencies": { - "core-js": "registry:dt/core-js#0.0.0+20160317120654", - "jasmine": "registry:dt/jasmine#2.2.0+20160505161446", - "node": "registry:dt/node#6.0.0+20160613154055" + "core-js": "registry:dt/core-js#0.0.0+20160602141332", + "jasmine": "registry:dt/jasmine#2.2.0+20160621224255", + "node": "registry:dt/node#6.0.0+20160621231320" } } diff --git a/public/docs/_examples/router/e2e-spec.ts b/public/docs/_examples/router/e2e-spec.ts index f86293e823..ac3e36f4ea 100644 --- a/public/docs/_examples/router/e2e-spec.ts +++ b/public/docs/_examples/router/e2e-spec.ts @@ -24,14 +24,18 @@ describe('Router', function () { heroDetail: element(by.css('my-app > undefined > div')), heroDetailTitle: element(by.css('my-app > undefined > div > h3')), + adminHref: hrefEles.get(2), + loginHref: hrefEles.get(3) }; } it('should be able to see the start screen', function () { let page = getPageStruct(); - expect(page.hrefs.count()).toEqual(2, 'should be two dashboard choices'); + expect(page.hrefs.count()).toEqual(4, 'should be two dashboard choices'); expect(page.crisisHref.getText()).toEqual('Crisis Center'); expect(page.heroesHref.getText()).toEqual('Heroes'); + expect(page.adminHref.getText()).toEqual('Crisis Admin'); + expect(page.loginHref.getText()).toEqual('Login'); }); it('should be able to see crises center items', function () { diff --git a/public/docs/_examples/router/ts/app/app.component.2.ts b/public/docs/_examples/router/ts/app/app.component.2.ts index 779d43a063..f1580cf6f8 100644 --- a/public/docs/_examples/router/ts/app/app.component.2.ts +++ b/public/docs/_examples/router/ts/app/app.component.2.ts @@ -3,7 +3,7 @@ // #docregion import { Component } from '@angular/core'; -import { Router, ROUTER_DIRECTIVES } from '@angular/router'; +import { ROUTER_DIRECTIVES } from '@angular/router'; // #enddocregion /* diff --git a/public/docs/_examples/router/ts/app/app.routes.1.ts b/public/docs/_examples/router/ts/app/app.routes.1.ts index 480b4eb95c..8c4caeda55 100644 --- a/public/docs/_examples/router/ts/app/app.routes.1.ts +++ b/public/docs/_examples/router/ts/app/app.routes.1.ts @@ -16,11 +16,11 @@ import { HeroDetailComponent } from './heroes/hero-detail.component'; // #docregion route-config export const routes: RouterConfig = [ // #docregion route-defs - { path: '/crisis-center', component: CrisisCenterComponent }, - { path: '/heroes', component: HeroListComponent }, + { path: 'crisis-center', component: CrisisCenterComponent }, + { path: 'heroes', component: HeroListComponent }, // #enddocregion route-defs // #docregion hero-detail-route - { path: '/hero/:id', component: HeroDetailComponent } + { path: 'hero/:id', component: HeroDetailComponent } // #enddocregion hero-detail-route ]; diff --git a/public/docs/_examples/router/ts/app/app.routes.2.ts b/public/docs/_examples/router/ts/app/app.routes.2.ts index 57c069c81d..d0d64e2ea0 100644 --- a/public/docs/_examples/router/ts/app/app.routes.2.ts +++ b/public/docs/_examples/router/ts/app/app.routes.2.ts @@ -9,8 +9,8 @@ import { HeroListComponent } from './hero-list.component'; // #docregion route-config export const routes: RouterConfig = [ - { path: '/crisis-center', component: CrisisListComponent }, - { path: '/heroes', component: HeroListComponent } + { path: 'crisis-center', component: CrisisListComponent }, + { path: 'heroes', component: HeroListComponent } ]; export const APP_ROUTER_PROVIDERS = [ diff --git a/public/docs/_examples/router/ts/app/app.routes.3.ts b/public/docs/_examples/router/ts/app/app.routes.3.ts index f7becf70ea..c6e9cffc72 100644 --- a/public/docs/_examples/router/ts/app/app.routes.3.ts +++ b/public/docs/_examples/router/ts/app/app.routes.3.ts @@ -7,7 +7,7 @@ import { HeroesRoutes } from './heroes/heroes.routes'; export const routes = [ ...HeroesRoutes, - { path: '/crisis-center', component: CrisisListComponent } + { path: 'crisis-center', component: CrisisListComponent } ]; export const APP_ROUTER_PROVIDERS = [ diff --git a/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.1.ts b/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.1.ts index 0bd846c600..0c50d6b7df 100644 --- a/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.1.ts +++ b/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.1.ts @@ -7,11 +7,11 @@ import { CrisisCenterComponent } from './crisis-center.component'; // #docregion routes export const CrisisCenterRoutes: RouterConfig = [ { - path: '/crisis-center', + path: 'crisis-center', component: CrisisCenterComponent, children: [ - { path: '/', component: CrisisListComponent }, - { path: '/:id', component: CrisisDetailComponent } + { path: ':id', component: CrisisDetailComponent }, + { path: '', component: CrisisListComponent } ] } ]; diff --git a/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.2.ts b/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.2.ts index 2856544136..43eadbd55e 100644 --- a/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.2.ts +++ b/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.2.ts @@ -6,17 +6,20 @@ import { CrisisCenterComponent } from './crisis-center.component'; // #docregion routes export const CrisisCenterRoutes: RouterConfig = [ + // #docregion redirect { - path: '/crisis-center', + path: '', + redirectTo: '/crisis-center', + terminal: true + }, + // #enddocregion redirect + { + path: 'crisis-center', component: CrisisCenterComponent, - index: true, children: [ - { path: '/:id', component: CrisisDetailComponent }, - { path: '/', component: CrisisListComponent, - index: true - } + { path: ':id', component: CrisisDetailComponent }, + { path: '', component: CrisisListComponent } ] } ]; // #enddocregion routes - diff --git a/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.3.ts b/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.3.ts index 5d7170d500..1f194ac6f7 100644 --- a/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.3.ts +++ b/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.3.ts @@ -10,25 +10,28 @@ import { CanDeactivateGuard } from '../interfaces'; export const CrisisCenterRoutes: RouterConfig = [ { - path: '/crisis-center', + path: '', + redirectTo: '/crisis-center', + terminal: true + }, + { + path: 'crisis-center', component: CrisisCenterComponent, - index: true, children: [ // #docregion admin-route-no-guard { - path: '/admin', + path: 'admin', component: CrisisAdminComponent }, // #enddocregion admin-route-no-guard { - path: '/:id', + path: ':id', component: CrisisDetailComponent, canDeactivate: [CanDeactivateGuard] }, { - path: '/', - component: CrisisListComponent, - index: true + path: '', + component: CrisisListComponent } ] } @@ -40,7 +43,7 @@ export const CrisisCenterRoutes: RouterConfig = [ import { AuthGuard } from '../auth.guard'; { - path: '/admin', + path: 'admin', component: CrisisAdminComponent, canActivate: [AuthGuard] } diff --git a/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.4.ts b/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.4.ts index 0e16b01d1c..f238e132a7 100644 --- a/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.4.ts +++ b/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.4.ts @@ -11,25 +11,28 @@ import { AuthGuard } from '../auth.guard'; export const CrisisCenterRoutes: RouterConfig = [ { - path: '/crisis-center', + path: '', + redirectTo: '/crisis-center', + terminal: true + }, + { + path: 'crisis-center', component: CrisisCenterComponent, - index: true, children: [ { - path: '/admin', + path: 'admin', component: CrisisAdminComponent, canActivate: [AuthGuard] }, { - path: '/:id', + path: ':id', component: CrisisDetailComponent, canDeactivate: [CanDeactivateGuard] }, // #docregion default-route { - path: '/', - component: CrisisListComponent, - index: true + path: '', + component: CrisisListComponent } // #enddocregion default-route ] @@ -42,7 +45,7 @@ export const CrisisCenterRoutes: RouterConfig = [ import { AuthGuard } from '../auth.guard'; { - path: '/admin', + path: 'admin', component: CrisisAdminComponent, canActivate: [AuthGuard] } diff --git a/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.ts b/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.ts index f9d0a26f8d..a924aa5771 100644 --- a/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.ts +++ b/public/docs/_examples/router/ts/app/crisis-center/crisis-center.routes.ts @@ -10,25 +10,29 @@ import { AuthGuard } from '../auth.guard'; export const CrisisCenterRoutes: RouterConfig = [ { - path: '/crisis-center', + path: '', + redirectTo: '/crisis-center', + terminal: true + }, + { + path: 'crisis-center', component: CrisisCenterComponent, - index: true, children: [ // #docregion admin-route { - path: '/admin', + path: 'admin', component: CrisisAdminComponent, canActivate: [AuthGuard] }, // #enddocregion admin-route { - path: '/:id', + path: ':id', component: CrisisDetailComponent, canDeactivate: [CanDeactivateGuard] }, - { path: '/', - component: CrisisListComponent, - index: true + { + path: '', + component: CrisisListComponent } ] } diff --git a/public/docs/_examples/router/ts/app/crisis-center/crisis.service.ts b/public/docs/_examples/router/ts/app/crisis-center/crisis.service.ts index 72e2500669..20597752cc 100644 --- a/public/docs/_examples/router/ts/app/crisis-center/crisis.service.ts +++ b/public/docs/_examples/router/ts/app/crisis-center/crisis.service.ts @@ -14,7 +14,7 @@ const CRISES = [ let crisesPromise = Promise.resolve(CRISES); // #docregion -import {Injectable} from '@angular/core'; +import { Injectable } from '@angular/core'; @Injectable() export class CrisisService { diff --git a/public/docs/_examples/router/ts/app/heroes/hero-detail.component.ts b/public/docs/_examples/router/ts/app/heroes/hero-detail.component.ts index 9f765d0ad0..bfa9ca6d0f 100644 --- a/public/docs/_examples/router/ts/app/heroes/hero-detail.component.ts +++ b/public/docs/_examples/router/ts/app/heroes/hero-detail.component.ts @@ -56,7 +56,7 @@ export class HeroDetailComponent implements OnInit, OnDestroy { let heroId = this.hero ? this.hero.id : null; // Pass along the hero id if available // so that the HeroList component can select that hero. - this.router.navigate(['/heroes'], { queryParams: { id: `${heroId}`, foo: 'foo' } }); + this.router.navigate(['/heroes'], { queryParams: { id: heroId, foo: 'foo' } }); } // #enddocregion gotoHeroes-navigate } diff --git a/public/docs/_examples/router/ts/app/heroes/heroes.routes.ts b/public/docs/_examples/router/ts/app/heroes/heroes.routes.ts index c4463cc8a1..80018d1f26 100644 --- a/public/docs/_examples/router/ts/app/heroes/heroes.routes.ts +++ b/public/docs/_examples/router/ts/app/heroes/heroes.routes.ts @@ -4,9 +4,9 @@ import { HeroListComponent } from './hero-list.component'; import { HeroDetailComponent } from './hero-detail.component'; export const HeroesRoutes: RouterConfig = [ - { path: '/heroes', component: HeroListComponent }, + { path: 'heroes', component: HeroListComponent }, // #docregion hero-detail-route - { path: '/hero/:id', component: HeroDetailComponent } + { path: 'hero/:id', component: HeroDetailComponent } // #enddocregion hero-detail-route ]; // #enddocregion diff --git a/public/docs/_examples/router/ts/app/login.component.ts b/public/docs/_examples/router/ts/app/login.component.ts index 898ca2d4c5..2790bfc79d 100755 --- a/public/docs/_examples/router/ts/app/login.component.ts +++ b/public/docs/_examples/router/ts/app/login.component.ts @@ -4,7 +4,6 @@ import { Router } from '@angular/router'; import { AuthService } from './auth.service'; @Component({ - selector: 'login', template: `

LOGIN

{{message}}

@@ -25,7 +24,7 @@ export class LoginComponent { } login() { - this.message = "Trying to log in ..."; + this.message = 'Trying to log in ...'; this.authService.login().subscribe(() => { this.setMessage(); diff --git a/public/docs/_examples/router/ts/app/login.routes.ts b/public/docs/_examples/router/ts/app/login.routes.ts index 71ad3cf9e4..6dabba376f 100644 --- a/public/docs/_examples/router/ts/app/login.routes.ts +++ b/public/docs/_examples/router/ts/app/login.routes.ts @@ -4,7 +4,7 @@ import { AuthService } from './auth.service'; import { LoginComponent } from './login.component'; export const LoginRoutes = [ - { path: '/login', component: LoginComponent } + { path: 'login', component: LoginComponent } ]; export const AUTH_PROVIDERS = [AuthGuard, AuthService]; diff --git a/public/docs/_examples/security/e2e-spec.ts b/public/docs/_examples/security/e2e-spec.ts new file mode 100644 index 0000000000..410f1428e1 --- /dev/null +++ b/public/docs/_examples/security/e2e-spec.ts @@ -0,0 +1,25 @@ +/// +'use strict'; +describe('Security E2E Tests', () => { + beforeAll(function() { browser.get(''); }); + + it('sanitizes innerHTML', () => { + let interpolated = element(By.className('e2e-inner-html-interpolated')); + expect(interpolated.getText()) + .toContain('Template Syntax'); + let bound = element(By.className('e2e-inner-html-bound')); + expect(bound.getText()).toContain('Template alert("0wned") Syntax'); + let bold = element(By.css('.e2e-inner-html-bound b')); + expect(bold.getText()).toContain('Syntax'); + }); + + it('binds trusted URLs', () => { + let dangerousUrl = element(By.className('e2e-dangerous-url')); + expect(dangerousUrl.getAttribute('href')).toMatch(/^javascript:alert/); + }); + + it('binds trusted resource URLs', () => { + let iframe = element(By.className('e2e-iframe')); + expect(iframe.getAttribute('src')).toMatch(/^https:\/\/www.youtube.com\//); + }); +}); diff --git a/public/docs/_examples/security/ts/app/app.component.ts b/public/docs/_examples/security/ts/app/app.component.ts new file mode 100644 index 0000000000..153e6b9e49 --- /dev/null +++ b/public/docs/_examples/security/ts/app/app.component.ts @@ -0,0 +1,20 @@ +// #docregion +import { Component } from '@angular/core'; + +import { BypassSecurityComponent } from './bypass-security.component'; +import { InnerHtmlBindingComponent } from './inner-html-binding.component'; + +@Component({ + selector: 'app-root', + template: ` +

Security

+ + + `, + directives: [ + BypassSecurityComponent, + InnerHtmlBindingComponent, + ], +}) +export class AppComponent { +} diff --git a/public/docs/_examples/security/ts/app/bypass-security.component.html b/public/docs/_examples/security/ts/app/bypass-security.component.html new file mode 100644 index 0000000000..75b7734dd4 --- /dev/null +++ b/public/docs/_examples/security/ts/app/bypass-security.component.html @@ -0,0 +1,15 @@ + +

Bypass Security Component

+ + +

A dangerous URL:

+

Click me.

+ + + +

Resource URL:

+

+ + + + diff --git a/public/docs/_examples/security/ts/app/bypass-security.component.ts b/public/docs/_examples/security/ts/app/bypass-security.component.ts new file mode 100644 index 0000000000..6a5f350d28 --- /dev/null +++ b/public/docs/_examples/security/ts/app/bypass-security.component.ts @@ -0,0 +1,33 @@ +// #docplaster +// #docregion +import { Component } from '@angular/core'; +import { DomSanitizationService, SafeResourceUrl, SafeUrl } from '@angular/platform-browser'; + +@Component({ + selector: 'bypass-security', + templateUrl: 'app/bypass-security.component.html', +}) +export class BypassSecurityComponent { + dangerousUrl: SafeUrl; + videoUrl: SafeResourceUrl; + + // #docregion trust-url + constructor(private sanitizer: DomSanitizationService) { + // javascript: URLs are dangerous if attacker controlled. Angular sanitizes them in data + // binding, but we can explicitly tell Angular to trust this value: + this.dangerousUrl = sanitizer.bypassSecurityTrustUrl('javascript:alert("Hi there")'); + // #enddocregion trust-url + this.updateVideoUrl('PUBnlbjZFAI'); + } + + // #docregion trust-video-url + updateVideoUrl(id: string) { + // Appending an ID to a YouTube URL is safe. + // Always make sure to construct SafeValue objects as close as possible to the input data, so + // that it's easier to check if the value is safe. + this.videoUrl = + this.sanitizer.bypassSecurityTrustResourceUrl('https://www.youtube.com/embed/' + id); + } + // #enddocregion trust-video-url +} +// #enddocregion diff --git a/public/docs/_examples/security/ts/app/inner-html-binding.component.html b/public/docs/_examples/security/ts/app/inner-html-binding.component.html new file mode 100644 index 0000000000..fe540d25fe --- /dev/null +++ b/public/docs/_examples/security/ts/app/inner-html-binding.component.html @@ -0,0 +1,6 @@ + +

Binding innerHTML

+

Bound value:

+

{{htmlSnippet}}

+

Result of binding to innerHTML:

+

diff --git a/public/docs/_examples/security/ts/app/inner-html-binding.component.ts b/public/docs/_examples/security/ts/app/inner-html-binding.component.ts new file mode 100644 index 0000000000..95a9f55979 --- /dev/null +++ b/public/docs/_examples/security/ts/app/inner-html-binding.component.ts @@ -0,0 +1,14 @@ +// #docregion +import { Component } from '@angular/core'; + +@Component({ + moduleId: module.id, + selector: 'inner-html-binding', + templateUrl: 'inner-html-binding.component.html', +}) +// #docregion inner-html-controller +export class InnerHtmlBindingComponent { + // E.g. a user/attacker controlled value from a URL. + htmlSnippet = 'Template Syntax'; +} +// #enddocregion inner-html-controller diff --git a/public/docs/_examples/security/ts/app/main.ts b/public/docs/_examples/security/ts/app/main.ts new file mode 100644 index 0000000000..3e1476beac --- /dev/null +++ b/public/docs/_examples/security/ts/app/main.ts @@ -0,0 +1,8 @@ +// #docregion +import { bootstrap } from '@angular/platform-browser-dynamic'; + +// #docregion import +import { AppComponent } from './app.component'; +// #enddocregion import + +bootstrap(AppComponent); diff --git a/public/docs/_examples/security/ts/example-config.json b/public/docs/_examples/security/ts/example-config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/public/docs/_examples/security/ts/index.html b/public/docs/_examples/security/ts/index.html new file mode 100644 index 0000000000..b665a8bc6f --- /dev/null +++ b/public/docs/_examples/security/ts/index.html @@ -0,0 +1,26 @@ + + + + + Angular Content Security + + + + + + + + + + + + + + + + + Loading... + + diff --git a/public/docs/_examples/security/ts/plnkr.json b/public/docs/_examples/security/ts/plnkr.json new file mode 100644 index 0000000000..e66527cb6b --- /dev/null +++ b/public/docs/_examples/security/ts/plnkr.json @@ -0,0 +1,8 @@ +{ + "description": "Content Security", + "files": [ + "!**/*.d.ts", + "!**/*.js" + ], + "tags": ["security"] +} diff --git a/public/docs/_examples/style-guide/ts/10-01/app/+dashboard/dashboard.component.ts b/public/docs/_examples/style-guide/ts/10-01/app/+dashboard/dashboard.component.ts deleted file mode 100644 index 925b3c0365..0000000000 --- a/public/docs/_examples/style-guide/ts/10-01/app/+dashboard/dashboard.component.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Component, OnInit } from '@angular/core'; - -@Component({ - selector: 'toh-dashboard', - templateUrl: 'app/dashboard/dashboard.component.html' -}) -export class DashboardComponent implements OnInit { - constructor() { } - - ngOnInit() { } -} diff --git a/public/docs/_examples/style-guide/ts/10-01/app/+dashboard/index.ts b/public/docs/_examples/style-guide/ts/10-01/app/+dashboard/index.ts deleted file mode 100644 index 28b2996161..0000000000 --- a/public/docs/_examples/style-guide/ts/10-01/app/+dashboard/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './dashboard.component'; diff --git a/public/docs/_examples/style-guide/ts/10-01/app/+heroes/heroes.component.ts b/public/docs/_examples/style-guide/ts/10-01/app/+heroes/heroes.component.ts deleted file mode 100644 index 569a25f688..0000000000 --- a/public/docs/_examples/style-guide/ts/10-01/app/+heroes/heroes.component.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Component, OnInit } from '@angular/core'; - -@Component({ - selector: 'toh-heroes', - templateUrl: 'app/heroes/heroes.component.html' -}) -export class HeroesComponent implements OnInit { - constructor() { } - - ngOnInit() { } -} diff --git a/public/docs/_examples/style-guide/ts/10-01/app/+heroes/index.ts b/public/docs/_examples/style-guide/ts/10-01/app/+heroes/index.ts deleted file mode 100644 index d85a0c81c4..0000000000 --- a/public/docs/_examples/style-guide/ts/10-01/app/+heroes/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './shared'; -export * from './heroes.component.ts'; diff --git a/public/docs/_examples/style-guide/ts/10-01/app/+heroes/shared/hero.service.ts b/public/docs/_examples/style-guide/ts/10-01/app/+heroes/shared/hero.service.ts deleted file mode 100644 index 6d9ec12949..0000000000 --- a/public/docs/_examples/style-guide/ts/10-01/app/+heroes/shared/hero.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@angular/core'; - -@Injectable() -export class HeroService { - - constructor() { } - -} diff --git a/public/docs/_examples/style-guide/ts/10-01/app/+heroes/shared/index.ts b/public/docs/_examples/style-guide/ts/10-01/app/+heroes/shared/index.ts deleted file mode 100644 index 9edcfff7fb..0000000000 --- a/public/docs/_examples/style-guide/ts/10-01/app/+heroes/shared/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './hero.service'; diff --git a/public/docs/_examples/style-guide/ts/10-01/app/app.component.ts b/public/docs/_examples/style-guide/ts/10-01/app/app.component.ts deleted file mode 100644 index 38d27afd5d..0000000000 --- a/public/docs/_examples/style-guide/ts/10-01/app/app.component.ts +++ /dev/null @@ -1,23 +0,0 @@ -// #docregion -import { Component } from '@angular/core'; -import { Routes, ROUTER_DIRECTIVES, ROUTER_PROVIDERS } from '@angular/router'; - -import { NavComponent } from './shared'; -import { DashboardComponent } from './+dashboard'; -import { HeroesComponent, HeroService } from './+heroes'; - -@Component({ - selector: 'toh-app', - templateUrl: 'app/app.component.html', - styleUrls: ['app/app.component.css'], - directives: [ROUTER_DIRECTIVES, NavComponent], - providers: [ - ROUTER_PROVIDERS, - HeroService - ] -}) -@Routes([ - { path: '/dashboard', component: DashboardComponent }, // , useAsDefault: true}, // coming soon - { path: '/heroes/...', component: HeroesComponent }, -]) -export class AppComponent {} diff --git a/public/docs/_examples/style-guide/ts/10-01/app/index.ts b/public/docs/_examples/style-guide/ts/10-01/app/index.ts deleted file mode 100644 index 033b419bdc..0000000000 --- a/public/docs/_examples/style-guide/ts/10-01/app/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './+dashboard'; -export * from './+heroes'; -export * from './shared'; -export * from './app.component'; diff --git a/public/docs/_examples/style-guide/ts/10-01/app/shared/index.ts b/public/docs/_examples/style-guide/ts/10-01/app/shared/index.ts deleted file mode 100644 index 38d6745899..0000000000 --- a/public/docs/_examples/style-guide/ts/10-01/app/shared/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './nav'; diff --git a/public/docs/_examples/style-guide/ts/10-01/app/shared/nav/index.ts b/public/docs/_examples/style-guide/ts/10-01/app/shared/nav/index.ts deleted file mode 100644 index ca588a3642..0000000000 --- a/public/docs/_examples/style-guide/ts/10-01/app/shared/nav/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './nav.component'; diff --git a/public/docs/_examples/style-guide/ts/10-01/app/shared/nav/nav.component.ts b/public/docs/_examples/style-guide/ts/10-01/app/shared/nav/nav.component.ts deleted file mode 100644 index 79dc0b0710..0000000000 --- a/public/docs/_examples/style-guide/ts/10-01/app/shared/nav/nav.component.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Component, OnInit } from '@angular/core'; - -@Component({ - selector: 'toh-nav', - templateUrl: 'app/shared/nav/nav.component.html' -}) -export class NavComponent implements OnInit { - constructor() { } - - ngOnInit() { } - -} diff --git a/public/docs/_examples/systemjs.config.js b/public/docs/_examples/systemjs.config.js index ca37f9e8e1..22b0c210cc 100644 --- a/public/docs/_examples/systemjs.config.js +++ b/public/docs/_examples/systemjs.config.js @@ -24,6 +24,7 @@ 'common', 'compiler', 'core', + 'forms', 'http', 'platform-browser', 'platform-browser-dynamic', diff --git a/public/docs/_examples/systemjs.config.plunker.js b/public/docs/_examples/systemjs.config.plunker.js index 0479c88703..40758797e6 100644 --- a/public/docs/_examples/systemjs.config.plunker.js +++ b/public/docs/_examples/systemjs.config.plunker.js @@ -5,8 +5,9 @@ */ (function(global) { - 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.3'; // lock router version + var ngVer = '@2.0.0-rc.3'; // 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.1'; // 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', diff --git a/public/docs/_examples/typings.json b/public/docs/_examples/typings.json index 9125b50685..3385926d1f 100644 --- a/public/docs/_examples/typings.json +++ b/public/docs/_examples/typings.json @@ -1,7 +1,7 @@ { "globalDependencies": { - "core-js": "registry:dt/core-js#0.0.0+20160317120654", - "jasmine": "registry:dt/jasmine#2.2.0+20160505161446", - "node": "registry:dt/node#6.0.0+20160613154055" + "core-js": "registry:dt/core-js#0.0.0+20160602141332", + "jasmine": "registry:dt/jasmine#2.2.0+20160621224255", + "node": "registry:dt/node#6.0.0+20160621231320" } } diff --git a/public/docs/_examples/upgrade-phonecat-2-hybrid/ts/systemjs.config.1.js b/public/docs/_examples/upgrade-phonecat-2-hybrid/ts/systemjs.config.1.js index a4134235b8..3661eb52a2 100644 --- a/public/docs/_examples/upgrade-phonecat-2-hybrid/ts/systemjs.config.1.js +++ b/public/docs/_examples/upgrade-phonecat-2-hybrid/ts/systemjs.config.1.js @@ -25,6 +25,7 @@ 'common', 'compiler', 'core', + 'forms', 'http', 'platform-browser', 'platform-browser-dynamic', diff --git a/public/docs/_examples/upgrade-phonecat-3-final/ts/systemjs.config.1.js b/public/docs/_examples/upgrade-phonecat-3-final/ts/systemjs.config.1.js index a4134235b8..3661eb52a2 100644 --- a/public/docs/_examples/upgrade-phonecat-3-final/ts/systemjs.config.1.js +++ b/public/docs/_examples/upgrade-phonecat-3-final/ts/systemjs.config.1.js @@ -25,6 +25,7 @@ 'common', 'compiler', 'core', + 'forms', 'http', 'platform-browser', 'platform-browser-dynamic', diff --git a/public/docs/_examples/webpack/ts/package.webpack.json b/public/docs/_examples/webpack/ts/package.webpack.json index 94e5525bd2..4064a86611 100644 --- a/public/docs/_examples/webpack/ts/package.webpack.json +++ b/public/docs/_examples/webpack/ts/package.webpack.json @@ -10,12 +10,13 @@ }, "license": "MIT", "dependencies": { - "@angular/common": "2.0.0-rc.2", - "@angular/compiler": "2.0.0-rc.2", - "@angular/core": "2.0.0-rc.2", - "@angular/http": "2.0.0-rc.2", - "@angular/platform-browser": "2.0.0-rc.2", - "@angular/platform-browser-dynamic": "2.0.0-rc.2", + "@angular/common": "2.0.0-rc.3", + "@angular/compiler": "2.0.0-rc.3", + "@angular/core": "2.0.0-rc.3", + "@angular/forms": "0.1.1", + "@angular/http": "2.0.0-rc.3", + "@angular/platform-browser": "2.0.0-rc.3", + "@angular/platform-browser-dynamic": "2.0.0-rc.3", "@angular/router-deprecated": "2.0.0-rc.2", "core-js": "^2.4.0", "reflect-metadata": "0.1.2", diff --git a/public/docs/_examples/webpack/ts/typings.1.json b/public/docs/_examples/webpack/ts/typings.1.json index 9125b50685..3385926d1f 100644 --- a/public/docs/_examples/webpack/ts/typings.1.json +++ b/public/docs/_examples/webpack/ts/typings.1.json @@ -1,7 +1,7 @@ { "globalDependencies": { - "core-js": "registry:dt/core-js#0.0.0+20160317120654", - "jasmine": "registry:dt/jasmine#2.2.0+20160505161446", - "node": "registry:dt/node#6.0.0+20160613154055" + "core-js": "registry:dt/core-js#0.0.0+20160602141332", + "jasmine": "registry:dt/jasmine#2.2.0+20160621224255", + "node": "registry:dt/node#6.0.0+20160621231320" } } diff --git a/public/docs/dart/latest/_data.json b/public/docs/dart/latest/_data.json index cf4039621f..fe6a0eb5ed 100644 --- a/public/docs/dart/latest/_data.json +++ b/public/docs/dart/latest/_data.json @@ -3,7 +3,7 @@ "icon": "home", "title": "Angular Docs", "menuTitle": "Docs Home", - "banner": "Welcome to angular.io/dart! The current Angular 2 release is beta.17. Consult the Change Log about recent enhancements, fixes, and breaking changes." + "banner": "Welcome to angular.io/dart! The current Angular 2 Dart release is beta.17. Consult the Change Log about recent enhancements, fixes, and breaking changes." }, "quickstart": { diff --git a/public/docs/dart/latest/guide/_data.json b/public/docs/dart/latest/guide/_data.json index eed8cf36a4..7d25438019 100644 --- a/public/docs/dart/latest/guide/_data.json +++ b/public/docs/dart/latest/guide/_data.json @@ -73,6 +73,11 @@ "intro": "Learn how to apply CSS styles to components." }, + "security": { + "title": "Security", + "intro": "Prevent security vulnerabilities" + }, + "hierarchical-dependency-injection": { "title": "Hierarchical Dependency Injectors", "navTitle": "Hierarchical Injectors", diff --git a/public/docs/dart/latest/guide/security.jade b/public/docs/dart/latest/guide/security.jade new file mode 100644 index 0000000000..f8df2a84a6 --- /dev/null +++ b/public/docs/dart/latest/guide/security.jade @@ -0,0 +1 @@ +!= partial("../../../_includes/_ts-temp") \ No newline at end of file diff --git a/public/docs/js/latest/_data.json b/public/docs/js/latest/_data.json index 0eb181d060..5227a9dd16 100644 --- a/public/docs/js/latest/_data.json +++ b/public/docs/js/latest/_data.json @@ -3,7 +3,7 @@ "icon": "home", "title": "Angular Docs", "menuTitle": "Docs Home", - "banner": "Welcome to Angular in JavaScript! The current Angular 2 release is rc.2. Please consult the Change Log about recent enhancements, fixes, and breaking changes." + "banner": "Welcome to Angular in JavaScript! The current Angular 2 release is rc.3. Please consult the Change Log about recent enhancements, fixes, and breaking changes." }, "quickstart": { diff --git a/public/docs/js/latest/guide/_data.json b/public/docs/js/latest/guide/_data.json index 939d7c5c4a..a0f8270eec 100644 --- a/public/docs/js/latest/guide/_data.json +++ b/public/docs/js/latest/guide/_data.json @@ -29,7 +29,7 @@ "basics": true }, - "forms": { + "forms-deprecated": { "title": "Forms", "intro": "A form creates a cohesive, effective, and compelling data entry experience. An Angular form coordinates a set of data-bound user controls, tracks changes, validates input, and presents errors.", "nextable": true, @@ -73,6 +73,11 @@ "intro": "Learn how to apply CSS styles to components." }, + "security": { + "title": "Security", + "intro": "Prevent security vulnerabilities" + }, + "hierarchical-dependency-injection": { "title": "Hierarchical Dependency Injectors", "navTitle": "Hierarchical Injectors", diff --git a/public/docs/js/latest/guide/forms-deprecated.jade b/public/docs/js/latest/guide/forms-deprecated.jade new file mode 100644 index 0000000000..185b25dee5 --- /dev/null +++ b/public/docs/js/latest/guide/forms-deprecated.jade @@ -0,0 +1,648 @@ +include ../_util-fns + +.alert.is-important + :marked + This guide is using the deprecated forms API. + + We have created a new version using the new API here. + +:marked + We’ve all used a form to login, submit a help request, place an order, book a flight, + schedule a meeting and perform countless other data entry tasks. + Forms are the mainstay of business applications. + + Any seasoned web developer can slap together an HTML form with all the right tags. + It's more challenging to create a cohesive data entry experience that guides the + user efficiently and effectively through the workflow behind the form. + + *That* takes design skills that are, to be frank, well out of scope for this chapter. + + It also takes framework support for + **two-way data binding, change tracking, validation, and error handling** + ... which we shall cover in this chapter on Angular forms. + + We will build a simple form from scratch, one step at a time. Along the way we'll learn + + - How to build an Angular form with a component and template + + - The `ngModel` two-way data binding syntax for reading and writing values to input controls + + - The `ngControl` directive to track the change state and validity of form controls + + - The special CSS classes that `ngControl` adds to form controls and how we can use them to provide strong visual feedback + + - How to display validation errors to users and enable/disable form controls + + - How to share information across controls with template local variables + + [Live Example](/resources/live-examples/forms-deprecated/js/plnkr.html) +.l-main-section +:marked + ## Template-Driven Forms + + Many of us will build forms by writing templates in the Angular [template syntax](./template-syntax.html) with + the form-specific directives and techniques described in this chapter. +.l-sub-section + :marked + That's not the only way to create a form but it's the way we'll cover in this chapter. +:marked + We can build almost any form we need with an Angular template — login forms, contact forms ... pretty much any business forms. + We can lay out the controls creatively, bind them to data, specify validation rules and display validation errors, + conditionally enable or disable specific controls, trigger built-in visual feedback, and much more. + + It will be pretty easy because Angular handles many of the repetitive, boiler plate tasks we'd + otherwise wrestle with ourselves. + + We'll discuss and learn to build the following template-driven form: + +figure.image-display + img(src="/resources/images/devguide/forms/hero-form-1.png" width="400px" alt="Clean Form") + +:marked + Here at the *Hero Employment Agency* we use this form to maintain personal information about the + heroes in our stable. Every hero needs a job. It's our company mission to match the right hero with the right crisis! + + Two of the three fields on this form are required. Required fields have a green bar on the left to make them easy to spot. + + If we delete the hero name, the form displays a validation error in an attention grabbing style: + +figure.image-display + img(src="/resources/images/devguide/forms/hero-form-2.png" width="400px" alt="Invalid, Name Required") + +:marked + Note that the submit button is disabled and the "required" bar to the left of the input control changed from green to red. + +.l-sub-section + p We'll' customize the colors and location of the "required" bar with standard CSS. + +:marked + We will build this form in the following sequence of small steps + + 1. Create the `Hero` model class + 1. Create the component that controls the form + 1. Create a template with the initial form layout + 1. Add the **ngModel** directive to each form input control + 1. Add the **ngControl** directive to each form input control + 1. Add custom CSS to provide visual feedback + 1. Show and hide validation error messages + 1. Handle form submission with **ngSubmit** + 1. Disable the form’s submit button until the form is valid + +:marked + ## Setup + Create a new project folder (`angular2-forms`) and follow the steps in the [QuickStart](../quickstart.html). + + ## Create the Hero Model Class + + As users enter form data, we capture their changes and update an instance of a model. + We can't layout the form until we know what the model looks like. + + A model can be as simple as a "property bag" that holds facts about a thing of application importance. + That describes well our `Hero` class with its three required fields (`id`, `name`, `power`) + and one optional field (`alterEgo`). + + Create a new file in the app folder called `hero.js` and give it the following constructor: + ++makeExample('forms-deprecated/js/app/hero.js', null, 'app/hero.js') + +:marked + It's an anemic model with few requirements and no behavior. Perfect for our demo. + + The `alterEgo` is optional and the constructor lets us omit it by being the last argument. + + We can create a new hero like this: +code-example(format=""). + var myHero = new Hero(42, 'SkyDog', + 'Fetch any object at any distance', 'Leslie Rollover'); + console.log('My hero is called ' + myHero.name); // "My hero is called SkyDog" +:marked + We update the `` of the `index.html` to include this javascript file. + ++makeExample('forms-deprecated/js/index.html', 'scripts-hero', 'index.html (excerpt)')(format=".") + +.l-main-section +:marked + ## Create a Form component + + An Angular form has two parts: an HTML-based template and a code-based Component to handle data and user interactions. + + We begin with the Component because it states, in brief, what the Hero editor can do. + + Create a new file called `hero-form.component.js` and give it the following definition: + ++makeExample('forms-deprecated/js/app/hero-form.component.js', 'first', 'app/hero-form.component.js') + +:marked + There’s nothing special about this component, nothing form-specific, nothing to distinguish it from any component we've written before. + + Understanding this component requires only the Angular 2 concepts we’ve learned in previous chapters + + 1. We use the `ng.core` object from the Angular library as we usually do. + + 1. The `Component()` selector value of "hero-form" means we can drop this form in a parent template with a `` tag. + + 1. The `templateUrl` property points to a separate file for template HTML called `hero-form.component.html`. + + 1. We defined dummy data for `model` and `powers` as befits a demo. + Down the road, we can inject a data service to get and save real data + or perhaps expose these properties as [inputs and outputs](./template-syntax.html#inputs-outputs) for binding to a + parent component. None of this concerns us now and these future changes won't affect our form. + + 1. We threw in a `diagnostic` method at the end to return a JSON representation of our model. + It'll help us see what we're doing during our development; we've left ourselves a cleanup note to discard it later. + + Why don't we write the template inline in the component file as we often do + elsewhere in the Developer Guide? + + There is no “right” answer for all occasions. We like inline templates when they are short. + Most form templates won't be short. TypeScript and JavaScript files generally aren't the best place to + write (or read) large stretches of HTML and few editors are much help with files that have a mix of HTML and code. + We also like short files with a clear and obvious purpose like this one. + + We made a good choice to put the HTML template elsewhere. + We'll write that template in a moment. Before we do, we'll take a step back + and revise the `app.component.js` to make use of our new `HeroFormComponent`. + +:marked + Again we update the `` of the `index.html` to include the new javascript file. + ++makeExample('forms-deprecated/js/index.html', 'scripts-hero-form', 'index.html (excerpt)')(format=".") + +.l-main-section +:marked + ## Revise the *app.component.js* + + `app.component.js` is the application's root component. It will host our new `HeroFormComponent`. + + Replace the contents of the "QuickStart" version with the following: ++makeExample('forms-deprecated/js/app/app.component.js', null, 'app/app.component.js') + +:marked +.l-sub-section + :marked + There are only two changes: + + 1. The `template` is simply the new element tag identified by the component's `select` property. + + 1. The `directives` array tells Angular that our template depends upon the `HeroFormComponent` + which is itself a Directive (as are all Components). + +.l-main-section +:marked + ## Create an initial HTML Form Template + + Create a new template file called `hero-form.component.html` and give it the following definition: + ++makeExample('forms-deprecated/js/app/hero-form.component.html', 'start', 'app/hero-form.component.html') + +:marked + That is plain old HTML 5. We're presenting two of the `Hero` fields, `name` and `alterEgo`, and + opening them up for user input in input boxes. + + The *Name* `` control has the HTML5 `required` attribute; + the *Alter Ego* `` control does not because `alterEgo` is optional. + + We've got a *Submit* button at the bottom with some classes on it. + + **We are not using Angular yet**. There are no bindings. No extra directives. Just layout. + + The `container`,`form-group`, `form-control`, and `btn` classes + come from [Twitter Boostrap](http://getbootstrap.com/css/). Purely cosmetic. + We're using Bootstrap to gussy up our form. + Hey, what's a form without a little style! + +.callout.is-important + header Angular Forms Do Not Require A Style Library + :marked + Angular makes no use of the `container`, `form-group`, `form-control`, and `btn` classes or + the styles of any external library. Angular apps can use any CSS library + ... or none at all. + +:marked + Let's add the stylesheet. + +ol + li Open a terminal window in the application root folder and enter the command: + code-example(language="html" escape="html"). + npm install bootstrap --save + li Open index.html and add the following link to the <head>. + +makeExample('forms-deprecated/js/index.html', 'bootstrap')(format=".") +:marked +.l-main-section +:marked + ## Add Powers with ***ngFor** + Our hero may choose one super power from a fixed list of Agency-approved powers. + We maintain that list internally (in `HeroFormComponent`). + + We'll add a `select` to our + form and bind the options to the `powers` list using `NgFor`, + a technique we might have seen before in the [Displaying Data](./displaying-data.html) chapter. + + Add the following HTML *immediately below* the *Alter Ego* group. ++makeExample('forms-deprecated/js/app/hero-form.component.html', 'powers', 'app/hero-form.component.html (excerpt)')(format=".") + +:marked + We are repeating the `` tag for each power in the list of Powers. + The `#p` local template variable is a different power in each iteration; + we display its name using the interpolation syntax with the double-curly-braces. + +.l-main-section +:marked + ## Two-way data binding with ***ngModel** + Running the app right now would be disappointing. + +figure.image-display + img(src="/resources/images/devguide/forms/hero-form-3.png" width="400px" alt="Early form with no binding") +:marked + We don't see hero data because we are not binding to the `Hero` yet. + We know how to do that from earlier chapters. + [Displaying Data](./displaying-data.html) taught us Property Binding. + [User Input](./user-input.html) showed us how to listen for DOM events with an + Event Binding and how to update a component property with the displayed value. + + Now we need to display, listen, and extract at the same time. + + We could use those techniques again in our form. + Instead we'll introduce something new, the `NgModel` directive, that + makes binding our form to the model super-easy. + + Find the `` tag for the "Name" and update it like this + ++makeExample('forms-deprecated/js/app/hero-form.component.html', 'ngModel-1','app/hero-form.component.html (excerpt)')(format=".") + +.l-sub-section + :marked + We appended a diagnostic interpolation after the input tag + so we can see what we're doing. + We left ourselves a note to throw it way when we're done. + +:marked + Focus on the binding syntax: `[(ngModel)]="..."`. + + If we ran the app right now and started typing in the *Name* input box, + adding and deleting characters, we'd see them appearing and disappearing + from the interpolated text. + At some point it might look like this. +figure.image-display + img(src="/resources/images/devguide/forms/ng-model-in-action.png" width="400px" alt="ngModel in action") +:marked + The diagnostic is evidence that we really are flowing values from the input box to the model and + back again. **That's two-way data binding!** + + Let's add similar `[(ngModel)]` bindings to *Alter Ego* and *Hero Power*. + We'll ditch the input box binding message + and add a new binding at the top to the component's `diagnostic` method. + Then we can confirm that two-way data binding works *for the entire Hero model*. + + After revision the core of our form should have three `[(ngModel)]` bindings that + look much like this: + ++makeExample('forms-deprecated/js/app/hero-form.component.html', 'ngModel-2', 'app/hero-form.component.html (excerpt)') + +:marked + If we ran the app right now and changed every Hero model property, the form might display like this: +figure.image-display + img(src="/resources/images/devguide/forms/ng-model-in-action-2.png" width="400px" alt="ngModel in super action") +:marked + The diagnostic near the top of the form + confirms that all of our changes are reflected in the model. + + **Delete** the `{{diagnostic()}}` binding at the top as it has served its purpose. + +.l-sub-section + :marked + ### Inside [(ngModel)] + *This section is an optional deep dive into [(ngModel)]. Not interested? Skip ahead!* + + The punctuation in the binding syntax, [()], is a good clue to what's going on. + + In a Property Binding, a value flows from the model to a target property on screen. + We identify that target property by surrounding its name in brackets, []. + This is a one-way data binding **from the model to the view**. + + In an Event Binding, we flow the value from the target property on screen to the model. + We identify that target property by surrounding its name in parentheses, (). + This is a one-way data binding in the opposite direction **from the view to the model**. + + No wonder Angular chose to combine the punctuation as [()] + to signify a two-way data binding and a **flow of data in both directions**. + + In fact, we can break the `NgModel` binding into its two separate modes + as we do in this re-write of the "Name" `` binding: + +makeExample('forms-deprecated/js/app/hero-form.component.html', 'ngModel-3','app/hero-form.component.html (excerpt)')(format=".") + + :marked +
The Property Binding should feel familiar. The Event Binding might seem strange. + + The `ngModelChange` is not an `` element event. + It is actually an event property of the `NgModel` directive. + When Angular sees a binding target in the form [(x)], + it expects the `x` directive to have an `x` input property and an `xChange` output property. + + The other oddity is the template expression, `model.name = $event`. + We're used to seeing an `$event` object coming from a DOM event. + The `ngModelChange` property doesn't produce a DOM event; it's an Angular `EventEmitter` + property that returns the input box value when it fires — which is precisely what + we should assign to the model's `name' property. + + Nice to know but is it practical? We almost always prefer `[(ngModel)]`. + We might split the binding if we had to do something special in + the event handling such as debounce or throttle the key strokes. + + Learn more about `NgModel` and other template syntax in the + [Template Syntax](./template-syntax.html) chapter. + +.l-main-section +:marked + ## Track change-state and validity with **ngControl** + + A form isn't just about data binding. We'd also like to know the state of the controls on our form. + The `NgControl` directive keeps track of control state for us. + +.callout.is-helpful + header NgControl requires Form + :marked + The `NgControl` is one of a family of `NgForm` directives that can only be applied to + a control within a ` tag. +:marked + Our application can ask an `NgControl` if the user touched the control, + if the value changed, or if the value became invalid. + + `NgControl` doesn't just track state; it updates the control with special + Angular CSS classes from the set we listed above. + We can leverage those class names to change the appearance of the + control and make messages appear or disappear. + + We'll explore those effects soon. Right now + we should **add `ngControl`to all three form controls**, + starting with the *Name* input box ++makeExample('forms-deprecated/js/app/hero-form.component.html', 'ngControl-1', 'app/hero-form.component.html (excerpt)')(format=".") +:marked + Be sure to assign a unique name to each `ngControl` directive. + +.l-sub-section + :marked + Angular registers controls under their `ngControl` names + with the `NgForm`. + We didn't add the `NgForm` directive explicitly but it's here + and we'll talk about it [later in this chapter](#ngForm). + +.l-main-section +:marked + ## Add Custom CSS for Visual Feedback + + `NgControl` doesn't just track state. + It updates the control with three classes that reflect the state. + +table + tr + th State + th Class if true + th Class if false + tr + td Control has been visited + td ng-touched + td ng-untouched + tr + td Control's value has changed + td ng-dirty + td ng-pristine + tr + td Control's value is valid + td ng-valid + td ng-invalid +:marked + Let's add a temporary [local template variable](./template-syntax.html#local-vars) named **spy** + to the "Name" `` tag and use the spy to display those classes. + ++makeExample('forms-deprecated/js/app/hero-form.component.html', 'ngControl-2','app/hero-form.component.html (excerpt)')(format=".") + +:marked + Now run the app and focus on the *Name* input box. + Follow the next four steps *precisely* + + 1. Look but don't touched + 1. Click in the input box, then click outside the text input box + 1. Add slashes to the end of the name + 1. Erase the name + + The actions and effects are as follows: +figure.image-display + img(src="/resources/images/devguide/forms/control-state-transitions-anim.gif" alt="Control State Transition") +:marked + We should be able to see the following four sets of class names and their transitions: +figure.image-display + img(src="/resources/images/devguide/forms/ng-control-class-changes.png" width="400px" alt="Control State Transitions") + +:marked + The (`ng-valid` | `ng-invalid`) pair are most interesting to us. We want to send a + strong visual signal when the data are invalid and we want to mark required fields. + + We realize we can do both at the same time with a colored bar on the left of the input box: + +figure.image-display + img(src="/resources/images/devguide/forms/validity-required-indicator.png" width="400px" alt="Invalid Form") + +:marked + We achieve this effect by adding two styles to a new `forms.css` file + that we add to our project as a sibling to `index.html`. + ++makeExample('forms-deprecated/js/forms.css',null,'forms.css')(format=".") +:marked + These styles select for the two Angular validity classes and the HTML 5 "required" attribute. + + We update the `` of the `index.html` to include this style sheet. ++makeExample('forms-deprecated/js/index.html', 'styles', 'index.html (excerpt)')(format=".") +:marked + ## Show and Hide Validation Error messages + + We can do better. + + The "Name" input box is required. Clearing it turns the bar red. That says *something* is wrong but we + don't know *what* is wrong or what to do about it. + We can leverage the `ng-invalid` class to reveal a helpful message. + + Here's the way it should look when the user deletes the name: +figure.image-display + img(src="/resources/images/devguide/forms/name-required-error.png" width="400px" alt="Name required") + +:marked + To achieve this effect we extend the `` tag with + 1. a [local template variable](./template-syntax.html#local-vars) + 1. the "*is required*" message in a nearby `
` which we'll display only if the control is invalid. + + Here's how we do it for the *name* input box: +-var stylePattern = { otl: /(#name="form")|(.*div.*$)|(Name is required)/gm }; ++makeExample('forms-deprecated/js/app/hero-form.component.html', + 'name-with-error-msg', + 'app/hero-form.component.html (excerpt)', + stylePattern) +:marked + When we added the `ngControl` directive, we bound it to the model's `name` property. + Here we initialize a template local variable (`name`) with the value "ngForm" (`#name="ngForm"`). + Angular recognizes that syntax and re-sets the `name` local template variable to the + `ngControl` directive instance. + In other words, the `name` local template variable becomes a handle on the `ngControl` object + for this input box. + + Now we can control visibility of the "name" error message by binding the message `
` element's `hidden` property + to the `ngControl` object's `valid` property. The message is hidden while the control is valid; + the message is revealed when the control becomes invalid. + +.l-sub-section + :marked + ### The NgForm directive + We just set a template local variable with the value of an `NgForm` directive. + Why did that work? We didn't add the **[`NgForm`](../api/common/NgForm-directive.html) directive** explicitly. + + Angular added it surreptitiously, wrapping it around the `
` element + + The `NgForm` directive supplements the `form` element with additional features. + It collects `Controls` (elements identified by an `ngControl` directive) + and monitors their properties including their validity. + It also has its own `valid` property which is true only if every contained + control is valid. +:marked + The Hero *Alter Ego* is optional so we can leave that be. + + Hero *Power* selection is required. + We can add the same kind of error handling to the `` tag. This is a requirement when using `[(ngModel)]` in combination with a form, so that we can easily refer to it in the aggregate form value and validity state. + Let's add similar `[(ngModel)]` bindings to *Alter Ego* and *Hero Power*. We'll ditch the input box binding message and add a new binding at the top to the component's `diagnostic` method. @@ -348,44 +373,33 @@ figure.image-display .l-main-section :marked - ## Track change-state and validity with **ngControl** + ## Track change-state and validity with **ngModel** A form isn't just about data binding. We'd also like to know the state of the controls on our form. - The `NgControl` directive keeps track of control state for us. -.callout.is-helpful - header NgControl requires Form - :marked - The `NgControl` is one of a family of `NgForm` directives that can only be applied to - a control within a ` tag. -:marked - Our application can ask an `NgControl` if the user touched the control, - if the value changed, or if the value became invalid. + Using `ngModel` in a form gives us more than just two way data binding. It also tells us if the user touched the control, if the value changed, or if the value became invalid. - `NgControl` doesn't just track state; it updates the control with special - Angular CSS classes from the set we listed above. + `ngModel` doesn't just track state; it updates the control with special Angular CSS classes from the set we listed above. We can leverage those class names to change the appearance of the control and make messages appear or disappear. We'll explore those effects soon. Right now - we should **add `ngControl`to all three form controls**, - starting with the *Name* input box -+makeExample('forms/js/app/hero-form.component.html', 'ngControl-1', 'app/hero-form.component.html (excerpt)')(format=".") + let's make sure we have `ngModel` and the corresponding name attribute on all three form controls, + starting with the *Name* input box. ++makeExample('forms/js/app/hero-form.component.html', 'ngModelName-1', 'app/hero-form.component.html (excerpt)')(format=".") :marked - Be sure to assign a unique name to each `ngControl` directive. + We set the `name` attribute to "name" which makes sense for our app. Any unique value will do. .l-sub-section :marked - Angular registers controls under their `ngControl` names - with the `NgForm`. - We didn't add the `NgForm` directive explicitly but it's here - and we'll talk about it [later in this chapter](#ngForm). + Internally Angular creates `FormControls` and registers them with an `NgForm` directive that Angular attached to the `` tag. Each `FormControl` is registered under the name we assigned to the `name` attribute. + We'll talk about `NgForm` [later in the chapter](#ngForm). .l-main-section :marked ## Add Custom CSS for Visual Feedback - `NgControl` doesn't just track state. + `NgModel` doesn't just track state. It updates the control with three classes that reflect the state. table @@ -409,7 +423,7 @@ table Let's add a temporary [local template variable](./template-syntax.html#local-vars) named **spy** to the "Name" `` tag and use the spy to display those classes. -+makeExample('forms/js/app/hero-form.component.html', 'ngControl-2','app/hero-form.component.html (excerpt)')(format=".") ++makeExample('forms/js/app/hero-form.component.html', 'ngModelName-2','app/hero-form.component.html (excerpt)')(format=".") :marked Now run the app and focus on the *Name* input box. @@ -472,16 +486,18 @@ figure.image-display 'app/hero-form.component.html (excerpt)', stylePattern) :marked - When we added the `ngControl` directive, we bound it to the model's `name` property. - Here we initialize a template local variable (`name`) with the value "ngForm" (`#name="ngForm"`). - Angular recognizes that syntax and re-sets the `name` local template variable to the - `ngControl` directive instance. - In other words, the `name` local template variable becomes a handle on the `ngControl` object - for this input box. - - Now we can control visibility of the "name" error message by binding the message `
` element's `hidden` property - to the `ngControl` object's `valid` property. The message is hidden while the control is valid; - the message is revealed when the control becomes invalid. + We need a template reference variable to access the input box's Angular control from within the template. + Here we created a variable called `name` and gave it the value "ngModel". +.l-sub-section + :marked + Why "ngModel"? + A directive's [exportAs](../api/core/DirectiveMetadata-class.html#!#exportAs) property + tells Angular how to link the reference variable to the directive. + We set `name` to `ngModel` because the `ngModel` directive's `exportAs` property happens to be "ngModel". + + Now we can control visibility of the "name" error message by binding properties of the `name` control to the message `
` element's `hidden` property. + + The message is hidden while the control is valid; the message is revealed when the control becomes invalid. .l-sub-section :marked @@ -492,7 +508,7 @@ figure.image-display Angular added it surreptitiously, wrapping it around the `` element The `NgForm` directive supplements the `form` element with additional features. - It collects `Controls` (elements identified by an `ngControl` directive) + It holds the controls we created for the elements with `ngModel` directive and `name` attribute and monitors their properties including their validity. It also has its own `valid` property which is true only if every contained control is valid. @@ -597,8 +613,7 @@ figure.image-display - A form component class with a `Component` decorator. - The `ngSubmit` directive for handling the form submission. - Template local variables such as `#heroForm`, `#name`, `#alter-ego` and `#power`. - - The `ngModel` directive for two-way data binding. - - The `ngControl` for validation and form element change tracking. + - The `[(ngModel)]` syntax for two-way data binding, validation and change tracking. - The local variable’s `valid` property on input controls to check if a control is valid and show/hide error messages. - Controlling the submit button's enabled state by binding to `NgForm` validity. - Custom CSS classes that provide visual feedback to users about invalid controls. diff --git a/public/docs/js/latest/guide/security.jade b/public/docs/js/latest/guide/security.jade new file mode 100644 index 0000000000..f8df2a84a6 --- /dev/null +++ b/public/docs/js/latest/guide/security.jade @@ -0,0 +1 @@ +!= partial("../../../_includes/_ts-temp") \ No newline at end of file diff --git a/public/docs/js/latest/quickstart.jade b/public/docs/js/latest/quickstart.jade index 73684f66e6..2edd48bb7a 100644 --- a/public/docs/js/latest/quickstart.jade +++ b/public/docs/js/latest/quickstart.jade @@ -31,9 +31,9 @@ figure.image-display .file app.component.js .file main.js .file index.html - .file license.md + .file styles.css :marked - Functionally, it's an `index.html` and two JavaScript files in an `app/` folder. + Functionally, it's an `index.html`, `styles.css` and two JavaScript files in an `app/` folder. We can handle that! Of course we won't build many apps that only run in plunker. @@ -43,6 +43,7 @@ figure.image-display 1. Write the Angular root component for our app 1. Bootstrap it to take control of the main web page 1. Write the main page (`index.html`) + 1. Add some CSS (`styles.css`) .l-sub-section :marked @@ -257,6 +258,16 @@ code-example(format=""). metadata, finds the `my-app` selector, locates an element tag named `my-app`, and loads our application between those tags. +:marked + ### Add some style + Styles aren't essential but they're nice, and `index.html` assumes we have + a stylesheet called `styles.css`. + + Create a `styles.css` file in the *#{_indexHtmlDir}* folder and start styling, perhaps with the minimal + styles shown below. For the full set of master styles used by the documentation samples, + see [styles.css](https://github.com/angular/angular.io/blob/master/public/docs/_examples/styles.css). ++makeExample('quickstart/js/styles.1.css', null, 'styles.css')(format=".") + .l-main-section :marked ## Run! @@ -306,15 +317,17 @@ figure.image-display .file main.js .file index.html .file package.json + .file styles.css :marked And here are the files: +makeTabs(` quickstart/js/app/app.component.js, quickstart/js/app/main.js, quickstart/js/index.html, - quickstart/js/package.1.json + quickstart/js/package.1.json, + quickstart/js/styles.1.css `,null, - `app/app.component.js, app/main.js, index.html,package.json`) + `app/app.component.js, app/main.js, index.html, package.json, styles.css`) :marked .l-main-section diff --git a/public/docs/ts/latest/_data.json b/public/docs/ts/latest/_data.json index b03e9827a9..d490737877 100644 --- a/public/docs/ts/latest/_data.json +++ b/public/docs/ts/latest/_data.json @@ -3,7 +3,7 @@ "icon": "home", "title": "Angular文档", "menuTitle": "文档首页", - "banner": "欢迎来到 Angular in TypeScript! 当前的Angular版本是 rc.2。请参考变更记录,以及最新功能,修复和重大打破性变化。" + "banner": "欢迎来到 Angular in TypeScript! 当前的Angular版本是 rc.3。请参考变更记录,以及最新功能,修复和重大打破性变化。" }, "cli-quickstart": { diff --git a/public/docs/ts/latest/cookbook/_data.json b/public/docs/ts/latest/cookbook/_data.json index 4f19461ed7..ac44f1dac2 100644 --- a/public/docs/ts/latest/cookbook/_data.json +++ b/public/docs/ts/latest/cookbook/_data.json @@ -25,8 +25,8 @@ "title": "依赖注入", "intro": "依赖注入技术" }, - "dynamic-form": { - "title": "动态表单", + "dynamic-form-deprecated": { + "title": "动态表单(已废弃)", "intro": "通过NgFormModel渲染动态表单" }, diff --git a/public/docs/ts/latest/cookbook/dynamic-form-deprecated.jade b/public/docs/ts/latest/cookbook/dynamic-form-deprecated.jade new file mode 100644 index 0000000000..2efa5f8197 --- /dev/null +++ b/public/docs/ts/latest/cookbook/dynamic-form-deprecated.jade @@ -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 here. + +: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*. + + +: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 + +: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='.') + + +: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 `` 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-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. + + +: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') + + +: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) diff --git a/public/docs/ts/latest/cookbook/dynamic-form.jade b/public/docs/ts/latest/cookbook/dynamic-form.jade index 55f19b0515..26e946fa87 100644 --- a/public/docs/ts/latest/cookbook/dynamic-form.jade +++ b/public/docs/ts/latest/cookbook/dynamic-form.jade @@ -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 here. + :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. @@ -9,12 +15,12 @@ include ../_util-fns 基于业务对象模型的元数据,动态创建表单可能会更划算。 - 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. - 在此烹饪宝典中,我们会展示如何利用`ngFormModel`来动态渲染一个简单的表单,包括各种控件类型和验证规则。 + 在此烹饪宝典中,我们会展示如何利用`formGroup`来动态渲染一个简单的表单,包括各种控件类型和验证规则。 这个起点很简陋,但可以在这个基础上添加丰富多彩的问卷问题、更优美的渲染以及更卓越的用户体验。 In our example we use a dynamic form to build an online application experience for heroes seeking employment. @@ -26,7 +32,13 @@ include ../_util-fns :marked ## Table of contents + ## 目录 + + [Bootstrap](#bootstrap) + + [程序启动](#bootstrap) + [Question Model](#object-model) [问卷问题模型](#object-model) @@ -48,13 +60,35 @@ include ../_util-fns **参见[在线例子](/resources/live-examples/cb-dynamic-form/ts/plnkr.html)**. + +.l-main-section + +:marked + ## Bootstrap + + ## 程序启动 + + During bootstrap we have to register the new forms module by calling `provideForms()` and pass the result to the provider array. + + 在程序启动时,我们要调用`provideForms()`,并把它的结果传给`providers`数组参数,来注册新的表单模块。 + ++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 :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. @@ -86,12 +120,13 @@ 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. - 接下来,我们定义了`QuestionControlService`,一个可以把问卷问题转换为一组ngForm控件的服务。 - 简而言之,这组ngForm控件使用问卷模型的元数据,并允许我们设置默认值和验证规则。 + 接下来,我们定义了`QuestionControlService`,一个可以把问卷问题转换为`FormGroup`的服务。 + 简而言之,这个`FormGroup`使用问卷模型的元数据,并允许我们设置默认值和验证规则。 + +makeExample('cb-dynamic-form/ts/app/question-control.service.ts',null,'app/question-control.service.ts')(format='.') @@ -137,10 +172,14 @@ include ../_util-fns 请注意,这个组件能代表模型里的任何问题类型。目前,还只有两种问题类型,但可以添加更多类型。可以用`ngSwitch`决定显示哪种类型的问题。 - 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. - 在这两个组件中,我们依赖Angular的**ngFormModel**来把模板HTML和底层控件对象连接起来,该对象从问卷问题模型里获取渲染和验证规则。 + 在这两个组件中,我们依赖Angular的**formGroup**来把模板HTML和底层控件对象连接起来,该对象从问卷问题模型里获取渲染和验证规则。 + + `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`. :marked diff --git a/public/docs/ts/latest/guide/_data.json b/public/docs/ts/latest/guide/_data.json index 68af4cc534..dd4f51f5b4 100644 --- a/public/docs/ts/latest/guide/_data.json +++ b/public/docs/ts/latest/guide/_data.json @@ -29,8 +29,8 @@ "basics": true }, - "forms": { - "title": "表单", + "forms-deprecated": { + "title": "表单(已废弃)", "intro": "表单创建一个有机、有效、引人注目的数据输入体验。Angular表单协调一组数据绑定控件,跟踪变更,验证输入的有效性,并且显示错误信息。", "nextable": true, "basics": true @@ -78,6 +78,11 @@ "intro": "学习如何给组件应用CSS样式。" }, + "security": { + "title": "Security", + "intro": "Developing for content security in Angular applications" + }, + "hierarchical-dependency-injection": { "title": "多级依赖注入器", "navTitle": "多级注入器", diff --git a/public/docs/ts/latest/guide/forms-deprecated.jade b/public/docs/ts/latest/guide/forms-deprecated.jade new file mode 100644 index 0000000000..ed3699f402 --- /dev/null +++ b/public/docs/ts/latest/guide/forms-deprecated.jade @@ -0,0 +1,718 @@ +include ../_util-fns + +.alert.is-important + :marked + This guide is using the deprecated forms API. + + We have created a new version using the new API here. + +:marked + We’ve all used a form to login, submit a help request, place an order, book a flight, + schedule a meeting and perform countless other data entry tasks. + Forms are the mainstay of business applications. + + Any seasoned web developer can slap together an HTML form with all the right tags. + It's more challenging to create a cohesive data entry experience that guides the + user efficiently and effectively through the workflow behind the form. + + *That* takes design skills that are, to be frank, well out of scope for this chapter. + + It also takes framework support for + **two-way data binding, change tracking, validation, and error handling** + ... which we shall cover in this chapter on Angular forms. + + We will build a simple form from scratch, one step at a time. Along the way we'll learn + + - to build an Angular form with a component and template + + - two-way data binding with `[(ngModel)]` syntax for reading and writing values to input controls + + - using `ngControl` to track the change state and validity of form controls + + - the special CSS classes that `ngControl` adds to form controls and how we can use them to provide strong visual feedback + + - displaying validation errors to users and enable/disable form controls + + - sharing information among controls with template reference variables + + [Live Example](/resources/live-examples/forms-deprecated/ts/plnkr.html) +.l-main-section +:marked + ## Template-Driven Forms + + Many of us will build forms by writing templates in the Angular [template syntax](./template-syntax.html) with + the form-specific directives and techniques described in this chapter. +.l-sub-section + :marked + That's not the only way to create a form but it's the way we'll cover in this chapter. +:marked + We can build almost any form we need with an Angular template — login forms, contact forms ... pretty much any business forms. + We can lay out the controls creatively, bind them to data, specify validation rules and display validation errors, + conditionally enable or disable specific controls, trigger built-in visual feedback, and much more. + + It will be pretty easy because Angular handles many of the repetitive, boiler plate tasks we'd + otherwise wrestle with ourselves. + + We'll discuss and learn to build the following template-driven form: + +figure.image-display + img(src="/resources/images/devguide/forms/hero-form-1.png" width="400px" alt="Clean Form") + +:marked + Here at the *Hero Employment Agency* we use this form to maintain personal information about the + heroes in our stable. Every hero needs a job. It's our company mission to match the right hero with the right crisis! + + Two of the three fields on this form are required. Required fields have a green bar on the left to make them easy to spot. + + If we delete the hero name, the form displays a validation error in an attention grabbing style: + +figure.image-display + img(src="/resources/images/devguide/forms/hero-form-2.png" width="400px" alt="Invalid, Name Required") + +:marked + Note that the submit button is disabled and the "required" bar to the left of the input control changed from green to red. + +.l-sub-section + p We'll customize the colors and location of the "required" bar with standard CSS. + +:marked + We will build this form in the following sequence of small steps + + 1. Create the `Hero` model class + 1. Create the component that controls the form + 1. Create a template with the initial form layout + 1. Bind data properties to each form input control with the `ngModel` two-way data binding syntax + 1. Add the **ngControl** directive to each form input control + 1. Add custom CSS to provide visual feedback + 1. Show and hide validation error messages + 1. Handle form submission with **ngSubmit** + 1. Disable the form’s submit button until the form is valid + +:marked + ## Setup + Create a new project folder (`angular2-forms`) and follow the steps in the [QuickStart](../quickstart.html). + +include ../_quickstart_repo +:marked + ## Create the Hero Model Class + + As users enter form data, we capture their changes and update an instance of a model. + We can't layout the form until we know what the model looks like. + + A model can be as simple as a "property bag" that holds facts about a thing of application importance. + That describes well our `Hero` class with its three required fields (`id`, `name`, `power`) + and one optional field (`alterEgo`). + + Create a new file in the app folder called `hero.ts` and give it the following class definition: + ++makeExample('forms-deprecated/ts/app/hero.ts', null, 'app/hero.ts') + +:marked + It's an anemic model with few requirements and no behavior. Perfect for our demo. + + The TypeScript compiler generates a public field for each `public` constructor parameter and + assigns the parameter’s value to that field automatically when we create new heroes. + + The `alterEgo` is optional and the constructor lets us omit it; note the (?) in `alterEgo?`. + + We can create a new hero like this: +code-example(format=""). + let myHero = new Hero(42, 'SkyDog', + 'Fetch any object at any distance', 'Leslie Rollover'); + console.log('My hero is called ' + myHero.name); // "My hero is called SkyDog" +:marked + +.l-main-section +:marked + ## Create a Form component + + An Angular form has two parts: an HTML-based template and a code-based Component to handle data and user interactions. + + We begin with the Component because it states, in brief, what the Hero editor can do. + + Create a new file called `hero-form.component.ts` and give it the following definition: + ++makeExample('forms-deprecated/ts/app/hero-form.component.ts', 'first', 'app/hero-form.component.ts') + +:marked + There’s nothing special about this component, nothing form-specific, nothing to distinguish it from any component we've written before. + + Understanding this component requires only the Angular 2 concepts we’ve learned in previous chapters + + 1. We import the `Component` decorator from the Angular library as we usually do. + + 1. The `@Component` selector value of "hero-form" means we can drop this form in a parent template with a `` tag. + + 1. The `templateUrl` property points to a separate file for template HTML called `hero-form.component.html`. + + 1. We defined dummy data for `model` and `powers` as befits a demo. + Down the road, we can inject a data service to get and save real data + or perhaps expose these properties as [inputs and outputs](./template-syntax.html#inputs-outputs) for binding to a + parent component. None of this concerns us now and these future changes won't affect our form. + + 1. We threw in a `diagnostic` property at the end to return a JSON representation of our model. + It'll help us see what we're doing during our development; we've left ourselves a cleanup note to discard it later. + + Why don't we write the template inline in the component file as we often do + elsewhere in the Developer Guide? + + There is no “right” answer for all occasions. We like inline templates when they are short. + Most form templates won't be short. TypeScript and JavaScript files generally aren't the best place to + write (or read) large stretches of HTML and few editors are much help with files that have a mix of HTML and code. + We also like short files with a clear and obvious purpose like this one. + + We made a good choice to put the HTML template elsewhere. + We'll write that template in a moment. Before we do, we'll take a step back + and revise the `app.component.ts` to make use of our new `HeroFormComponent`. + +.l-main-section +:marked + ## Revise the *app.component.ts* + + `app.component.ts` is the application's root component. It will host our new `HeroFormComponent`. + + Replace the contents of the "QuickStart" version with the following: ++makeExample('forms-deprecated/ts/app/app.component.ts', null, 'app/app.component.ts') + +:marked +.l-sub-section + :marked + There are only three changes: + + 1. We import the new `HeroFormComponent`. + + 1. The `template` is simply the new element tag identified by the component's `selector` property. + + 1. The `directives` array tells Angular that our template depends upon the `HeroFormComponent` + which is itself a Directive (as are all Components). + +.l-main-section +:marked + ## Create an initial HTML Form Template + + Create a new template file called `hero-form.component.html` and give it the following definition: + ++makeExample('forms-deprecated/ts/app/hero-form.component.html', 'start', 'app/hero-form.component.html') + +:marked + That is plain old HTML 5. We're presenting two of the `Hero` fields, `name` and `alterEgo`, and + opening them up for user input in input boxes. + + The *Name* `` control has the HTML5 `required` attribute; + the *Alter Ego* `` control does not because `alterEgo` is optional. + + We've got a *Submit* button at the bottom with some classes on it. + + **We are not using Angular yet**. There are no bindings. No extra directives. Just layout. + + The `container`,`form-group`, `form-control`, and `btn` classes + come from [Twitter Bootstrap](http://getbootstrap.com/css/). Purely cosmetic. + We're using Bootstrap to gussy up our form. + Hey, what's a form without a little style! + +.callout.is-important + header Angular Forms Do Not Require A Style Library + :marked + Angular makes no use of the `container`, `form-group`, `form-control`, and `btn` classes or + the styles of any external library. Angular apps can use any CSS library + ... or none at all. + +:marked + Let's add the stylesheet. + +ol + li Open a terminal window in the application root folder and enter the command: + code-example(language="html" escape="html"). + npm install bootstrap --save + li Open index.html and add the following link to the <head>. + +makeExample('forms-deprecated/ts/index.html', 'bootstrap')(format=".") +:marked +.l-main-section +:marked + ## Add Powers with ***ngFor** + Our hero may choose one super power from a fixed list of Agency-approved powers. + We maintain that list internally (in `HeroFormComponent`). + + We'll add a `select` to our + form and bind the options to the `powers` list using `ngFor`, + a technique we might have seen before in the [Displaying Data](./displaying-data.html) chapter. + + Add the following HTML *immediately below* the *Alter Ego* group. ++makeExample('forms-deprecated/ts/app/hero-form.component.html', 'powers', 'app/hero-form.component.html (excerpt)')(format=".") + +:marked + We are repeating the `` tag for each power in the list of Powers. + The `p` template input variable is a different power in each iteration; + we display its name using the interpolation syntax with the double-curly-braces. + + +.l-main-section +:marked + ## Two-way data binding with **ngModel** + Running the app right now would be disappointing. + +figure.image-display + img(src="/resources/images/devguide/forms/hero-form-3.png" width="400px" alt="Early form with no binding") +:marked + We don't see hero data because we are not binding to the `Hero` yet. + We know how to do that from earlier chapters. + [Displaying Data](./displaying-data.html) taught us Property Binding. + [User Input](./user-input.html) showed us how to listen for DOM events with an + Event Binding and how to update a component property with the displayed value. + + Now we need to display, listen, and extract at the same time. + + We could use those techniques again in our form. + Instead we'll introduce something new, the `[(ngModel)]` syntax, that + makes binding our form to the model super-easy. + + Find the `` tag for the "Name" and update it like this + ++makeExample('forms-deprecated/ts/app/hero-form.component.html', 'ngModel-1','app/hero-form.component.html (excerpt)')(format=".") + +.l-sub-section + :marked + We appended a diagnostic interpolation after the input tag + so we can see what we're doing. + We left ourselves a note to throw it away when we're done. + +:marked + Focus on the binding syntax: `[(ngModel)]="..."`. + + If we ran the app right now and started typing in the *Name* input box, + adding and deleting characters, we'd see them appearing and disappearing + from the interpolated text. + At some point it might look like this. +figure.image-display + img(src="/resources/images/devguide/forms/ng-model-in-action.png" width="400px" alt="ngModel in action") +:marked + The diagnostic is evidence that we really are flowing values from the input box to the model and + back again. **That's two-way data binding!** + + Let's add similar `[(ngModel)]` bindings to *Alter Ego* and *Hero Power*. + We'll ditch the input box binding message + and add a new binding at the top to the component's `diagnostic` property. + Then we can confirm that two-way data binding works *for the entire Hero model*. + + After revision the core of our form should have three `[(ngModel)]` bindings that + look much like this: + ++makeExample('forms-deprecated/ts/app/hero-form.component.html', 'ngModel-2', 'app/hero-form.component.html (excerpt)') + +:marked + If we ran the app right now and changed every Hero model property, the form might display like this: +figure.image-display + img(src="/resources/images/devguide/forms/ng-model-in-action-2.png" width="400px" alt="ngModel in super action") +:marked + The diagnostic near the top of the form + confirms that all of our changes are reflected in the model. + + **Delete** the `{{diagnostic}}` binding at the top as it has served its purpose. + +.l-sub-section + :marked + ### Inside [(ngModel)] + *This section is an optional deep dive into [(ngModel)]. Not interested? Skip ahead!* + + The punctuation in the binding syntax, [()], is a good clue to what's going on. + + In a Property Binding, a value flows from the model to a target property on screen. + We identify that target property by surrounding its name in brackets, []. + This is a one-way data binding **from the model to the view**. + + In an Event Binding, we flow the value from the target property on screen to the model. + We identify that target property by surrounding its name in parentheses, (). + This is a one-way data binding in the opposite direction **from the view to the model**. + + No wonder Angular chose to combine the punctuation as [()] + to signify a two-way data binding and a **flow of data in both directions**. + + In fact, we can break the `NgModel` binding into its two separate modes + as we do in this re-write of the "Name" `` binding: + +makeExample('forms-deprecated/ts/app/hero-form.component.html', 'ngModel-3','app/hero-form.component.html (excerpt)')(format=".") + + :marked +
The Property Binding should feel familiar. The Event Binding might seem strange. + + The `ngModelChange` is not an `` element event. + It is actually an event property of the `NgModel` directive. + When Angular sees a binding target in the form [(x)], + it expects the `x` directive to have an `x` input property and an `xChange` output property. + + The other oddity is the template expression, `model.name = $event`. + We're used to seeing an `$event` object coming from a DOM event. + The `ngModelChange` property doesn't produce a DOM event; it's an Angular `EventEmitter` + property that returns the input box value when it fires — which is precisely what + we should assign to the model's `name` property. + + Nice to know but is it practical? We almost always prefer `[(ngModel)]`. + We might split the binding if we had to do something special in + the event handling such as debounce or throttle the key strokes. + + Learn more about `NgModel` and other template syntax in the + [Template Syntax](./template-syntax.html) chapter. + +.l-main-section +:marked + ## Track change-state and validity with **ngControl** + + A form isn't just about data binding. We'd also like to know the state of the controls on our form. + + By setting `ngControl` we create a directive that can tell if the user touched the control, + if the value changed, or if the value became invalid. + + This directive doesn't just track state; it updates the control with special + Angular CSS classes from the set we listed above. + We can leverage those class names to change the appearance of the + control and make messages appear or disappear. + + We'll explore those effects soon. Right now + we should **add `ngControl` to all three form controls**, + starting with the *Name* input box ++makeExample('forms-deprecated/ts/app/hero-form.component.html', 'ngControl-1', 'app/hero-form.component.html (excerpt)')(format=".") +:marked + We set this particular `ngControl` to "name" which makes sense for our app. Any unique value will do. + +.l-sub-section + :marked + Internally Angular creates `Controls` and registers them under their `ngControl` names + with an `NgForm` directive that Angular attached to the `` tag. + We'll talk about `NgForm` [later in the chapter](#ngForm). + + The `ngControl` *attribute* in our template actually maps to the + [NgControlName](../api/common/NgControlName-directive.html) directive. + There is also a `NgControl` *abstract* directive which is *not the same thing*. + We often ignore this technical distinction and refer to `NgControlName` more conveniently (albeit incorrectly) as the *NgControl* directive. + + While we're under the hood, we might as well note that the `ngModel` in the + two-way binding syntax is now a property of the `NgControlName` directive. + The `NgModel` directive is no longer involved. We only need one directive to manage the DOM element + and there is no practical difference in the way either directive handles data binding. + +.l-main-section +:marked + ## Add Custom CSS for Visual Feedback + + The *NgControl* directive doesn't just track state. + It updates the control with three classes that reflect the state. + +table + tr + th State + th Class if true + th Class if false + tr + td Control has been visited + td ng-touched + td ng-untouched + tr + td Control's value has changed + td ng-dirty + td ng-pristine + tr + td Control's value is valid + td ng-valid + td ng-invalid +:marked + Let's add a temporary [template reference variable](./template-syntax.html#ref-vars) named **spy** + to the "Name" `` tag and use the spy to display those classes. + ++makeExample('forms-deprecated/ts/app/hero-form.component.html', 'ngControl-2','app/hero-form.component.html (excerpt)')(format=".") + +:marked + Now run the app and focus on the *Name* input box. + Follow the next four steps *precisely* + + 1. Look but don't touch + 1. Click in the input box, then click outside the text input box + 1. Add slashes to the end of the name + 1. Erase the name + + The actions and effects are as follows: +figure.image-display + img(src="/resources/images/devguide/forms/control-state-transitions-anim.gif" alt="Control State Transition") +:marked + We should be able to see the following four sets of class names and their transitions: +figure.image-display + img(src="/resources/images/devguide/forms/ng-control-class-changes.png" width="400px" alt="Control State Transitions") + +:marked + The (`ng-valid` | `ng-invalid`) pair are most interesting to us. We want to send a + strong visual signal when the data are invalid and we want to mark required fields. + + We realize we can do both at the same time with a colored bar on the left of the input box: + +figure.image-display + img(src="/resources/images/devguide/forms/validity-required-indicator.png" width="400px" alt="Invalid Form") + +:marked + We achieve this effect by adding two styles to a new `forms.css` file + that we add to our project as a sibling to `index.html`. + ++makeExample('forms-deprecated/ts/forms.css',null,'forms.css')(format=".") +:marked + These styles select for the two Angular validity classes and the HTML 5 "required" attribute. + + We update the `` of the `index.html` to include this style sheet. ++makeExample('forms-deprecated/ts/index.html', 'styles', 'index.html (excerpt)')(format=".") +:marked + ## Show and Hide Validation Error messages + + We can do better. + + The "Name" input box is required. Clearing it turns the bar red. That says *something* is wrong but we + don't know *what* is wrong or what to do about it. + We can leverage the `ng-invalid` class to reveal a helpful message. + + Here's the way it should look when the user deletes the name: +figure.image-display + img(src="/resources/images/devguide/forms/name-required-error.png" width="400px" alt="Name required") + +:marked + To achieve this effect we extend the `` tag with + 1. a [template reference variable](./template-syntax.html#ref-vars) + 1. the "*is required*" message in a nearby `
` which we'll display only if the control is invalid. + + Here's how we do it for the *name* input box: ++makeExample('forms-deprecated/ts/app/hero-form.component.html', + 'name-with-error-msg', + 'app/hero-form.component.html (excerpt)')(format=".") +:marked + We need a template reference variable to access the input box's Angular control from within the template. + Here we created a variable called `name` and gave it the value "ngForm". +.l-sub-section + :marked + Why "ngForm"? + A directive's [exportAs](../api/core/DirectiveMetadata-class.html#!#exportAs) property + tells Angular how to link the reference variable to the directive. + We set `name` to `ngForm` because the `NgControlName` directive's `exportAs` property happens to be "ngForm". + + This seems unintuitive at first until we realize that *all* control directives in the + Angular form family — including `NgForm`, `NgModel`, `NgControlName` and `NgControlGroup` — *exportAs* "ngForm" + and we only ever apply *one* of these directives to an element tag. + Consistency rules! + + Now we can control visibility of the "name" error message by binding properties of the `name` control to the message `
` element's `hidden` property. ++makeExample('forms-deprecated/ts/app/hero-form.component.html', + 'hidden-error-msg', + 'app/hero-form.component.html (excerpt)') +:marked + In this example, we hide the message when the control is valid or pristine; + pristine means the user hasn't changed the value since it was displayed in this form. + + This user experience is the developer's choice. Some folks want to see the message at all times. + If we ignore the `pristine` state, we would hide the message only when the value is valid. + If we arrive in this component with a new (blank) hero or an invalid hero, + we'll see the error message immediately, before we've done anything. + + Some folks find that behavior disconcerting. They only want to see the message when the user makes an invalid change. + Hiding the message while the control is "pristine" achieves that goal. + We'll see the significance of this choice when we [add a new hero](#new-hero) to the form. + + The Hero *Alter Ego* is optional so we can leave that be. + + Hero *Power* selection is required. + We can add the same kind of error handling to the `` tag. This is a requirement when using `[(ngModel)]` in combination with a form, so that we can easily refer to it in the aggregate form value and validity state. + Let's add similar `[(ngModel)]` bindings to *Alter Ego* and *Hero Power*. We'll ditch the input box binding message and add a new binding at the top to the component's `diagnostic` property. @@ -622,21 +665,19 @@ figure.image-display .l-main-section :marked - ## Track change-state and validity with **ngControl** + ## Track change-state and validity with **ngModel** - ## 通过**ngControl**跟踪修改状态与有效性验证 + ## 通过**ngModel**跟踪修改状态与有效性验证 A form isn't just about data binding. We'd also like to know the state of the controls on our form. 表单不仅是关于数据绑定的。我们还希望知道表单中各个控件的状态。 - By setting `ngControl` we create a directive that can tell if the user touched the control, - if the value changed, or if the value became invalid. + Using `ngModel` in a form gives us more than just two way data binding. It also tells us if the user touched the control, if the value changed, or if the value became invalid. - 通过设置`ngControl`,我们创建了一个指令,它能告诉我们用户是否接触过此控件、它的值是否变化了,或者它的值是否变得无效了。 + 在表单中使用`ngModel`能让我们比仅使用双向数据绑定获得更多的控制权。它还会告诉我们很多信息:用户碰过此控件吗?它的值变化了吗?数据变得无效了吗? - This directive doesn't just track state; it updates the control with special - Angular CSS classes from the set we listed above. + `ngModel` doesn't just track state; it updates the control with special Angular CSS classes from the set we listed above. We can leverage those class names to change the appearance of the control and make messages appear or disappear. @@ -644,41 +685,22 @@ figure.image-display 我们可以通过定制这些CSS类的样式来更改控件的外观,以及让消息被显示或隐藏。 We'll explore those effects soon. Right now - we should **add `ngControl` to all three form controls**, - starting with the *Name* input box + let's make sure we have `ngModel` and the corresponding name attribute on all three form controls, + starting with the *Name* input box. - 我们很快就会看到那些效果。现在,我们得先**把`ngControl`添加到所有这三个表单控件中**,就从*Name*输入框开始吧: -+makeExample('forms/ts/app/hero-form.component.html', 'ngControl-1', 'app/hero-form.component.html (节选)')(format=".") + 我们很快就会看到那些效果。我们首先要确保在所有这三个表单控件中都有`ngModel`以及相应的`name`属性。那就从*Name*输入框开始吧: + ++makeExample('forms/ts/app/hero-form.component.html', 'ngModelName-1', 'app/hero-form.component.html (excerpt)')(format=".") + :marked - We set this particular `ngControl` to "name" which makes sense for our app. Any unique value will do. + We set the `name` attribute to "name" which makes sense for our app. Any unique value will do. - 对本应用来说,把这个`ngControl`赋值为"name"会更容易理解。但也可以设置成任何唯一的值。 + 对本应用来说,把这个`name`属性设置为"name"会更容易理解。但也可以设置成任何唯一的值。 .l-sub-section :marked - Internally Angular creates `Controls` and registers them under their `ngControl` names - with an `NgForm` directive that Angular attached to the `` tag. + Internally Angular creates `FormControls` and registers them with an `NgForm` directive that Angular attached to the `` tag. Each `FormControl` is registered under the name we assigned to the `name` attribute. We'll talk about `NgForm` [later in the chapter](#ngForm). - - Angular会在内部创建`Controls`,并以`ngControl`的值为名字注册到`NgForm`指令中,这个指令被Angular自动附加到了``标签上。 - 我们将在[稍后的章节](#ngForm)中展开讨论`NgForm`。 - - The `ngControl` *attribute* in our template actually maps to the - [NgControlName](../api/common/NgControlName-directive.html) directive. - There is also a `NgControl` *abstract* directive which is *not the same thing*. - We often ignore this technical distinction and refer to `NgControlName` more conveniently (albeit incorrectly) as the *NgControl* directive. - - 我们模板中的这个`ngControl`*属性*实际上被映射到了 [NgControlName](../api/common/index/NgControlName-directive.html)指令。 - 虽然还有另一个叫`NgControl`的*抽象*指令,但和这个属性不是一回事。 - 我们通常会忽略这点技术上的差异。为了更方便(尽管不正确),我们提起`NgControlName`时还是会把它叫做*NgControl*指令。 - - While we're under the hood, we might as well note that the `ngModel` in the - two-way binding syntax is now a property of the `NgControlName` directive. - The `NgModel` directive is no longer involved. We only need one directive to manage the DOM element - and there is no practical difference in the way either directive handles data binding. - - 既然已经来到了技术底层,我们可能还注意到了双向绑定语法中的`ngModel`实际上是`NgControlName`指令的一个属性。 - `NgModel`指令不再需要了。我们只需要一个指令来管理DOM元素就够了,而且这两个指令在处理数据绑定时并没有实质性的差异。 .l-main-section :marked @@ -686,7 +708,7 @@ figure.image-display ## 添加自定义CSS以提供视觉反馈 - The *NgControl* directive doesn't just track state. + The *NgModel* directive doesn't just track state. It updates the control with three classes that reflect the state. *NgControl*指令不仅仅跟踪状态。它还使用三个CSS类来更新控件,以便反映当前状态。 @@ -736,7 +758,7 @@ table 我们往姓名``标签上添加一个名叫**spy**的临时[模板引用变量](./template-syntax.html#local-vars),然后用这个spy来显示它上面的所有css类。 -+makeExample('forms/ts/app/hero-form.component.html', 'ngControl-2','app/hero-form.component.html (节选)')(format=".") ++makeExample('forms/ts/app/hero-form.component.html', 'ngModelName-2','app/hero-form.component.html (excerpt)')(format=".") :marked Now run the app and focus on the *Name* input box. @@ -849,30 +871,20 @@ figure.image-display 'app/hero-form.component.html (节选)')(format=".") :marked We need a template reference variable to access the input box's Angular control from within the template. - Here we created a variable called `name` and gave it the value "ngForm". + Here we created a variable called `name` and gave it the value "ngModel". 我们需要一个模板引用变量来访问模板中输入框的Angular控件。 - 这里,我们创建了一个名叫`name`的变量,并且把它赋值为"ngForm"。 + 这里,我们创建了一个名叫`name`的变量,并且把它赋值为"ngModel"。 .l-sub-section :marked - Why "ngForm"? + Why "ngModel"? A directive's [exportAs](../api/core/DirectiveMetadata-class.html#!#exportAs) property tells Angular how to link the reference variable to the directive. - We set `name` to `ngForm` because the `NgControlName` directive's `exportAs` property happens to be "ngForm". + We set `name` to `ngModel` because the `ngModel` directive's `exportAs` property happens to be "ngModel". - 为什么是"ngForm"? - 指令的[exportAs](../api/core/index/DirectiveMetadata-class.html#!#exportAs)属性告诉Angular如何把局部变量链接到指令中。 - 这里我们把`name`设置为`ngForm`就是因为`NgControlName`指令的`exportAs`属性设置为了“ngForm”。 - - This seems unintuitive at first until we realize that *all* control directives in the - Angular form family — including `NgForm`, `NgModel`, `NgControlName` and `NgControlGroup` — *exportAs* "ngForm" - and we only ever apply *one* of these directives to an element tag. - Consistency rules! - - 起初这看起来不够直观,直到我们看到Angular表单家族下的*所有*控件指令 —— 包括`NgForm`、 - `NgModel`、`NgControlName`和`NgControlGroup` —— 都*exportAs*了 “ngForm”就会明白, - 原来我们刚才不过是把这些指令*中的一个*用在了一个元素标签上。 - 一致性统治一切! + 为什么是"ngModel"? + 指令的[exportAs](../api/core/index/DirectiveMetadata-class.html#!#exportAs)属性告诉Angular如何把模板引用变量链接到指令中。 + 这里我们把`name`设置为`ngModel`就是因为`ngModel`指令的`exportAs`属性设置成了“ngModel”。 Now we can control visibility of the "name" error message by binding properties of the `name` control to the message `
` element's `hidden` property. @@ -1045,7 +1057,7 @@ figure.image-display Angular替我们做了。Angular自动创建了`NgForm`指令,并且把它附加到``标签上。 The `NgForm` directive supplements the `form` element with additional features. - It holds the controls we created for the elements with `ngControl` attributes + It holds the controls we created for the elements with `ngModel` directive and `name` attribute and monitors their properties including their validity. It also has its own `valid` property which is true only *if every contained control* is valid. @@ -1191,13 +1203,9 @@ figure.image-display - 模板引用变量,如`#heroForm`、`#name`、`#alter-ego`和`#power`。 - - The `[(ngModel)]` syntax for two-way data binding. + - The `[(ngModel)]` syntax for two-way data binding, validation and change tracking. - - 用于双向数据绑定的`[(ngModel)]`语法 - - - The `ngControlName` directive for validation and form element change tracking. - - - 用于验证和表单元素变化跟踪的`ngControlName`指令 + - 用于双向数据绑定、数据验证和变化追踪的`[(ngModel)]`语法 - The reference variable’s `valid` property on input controls to check if a control is valid and show/hide error messages. diff --git a/public/docs/ts/latest/guide/router.jade b/public/docs/ts/latest/guide/router.jade index 853f735d65..a92d0386c0 100644 --- a/public/docs/ts/latest/guide/router.jade +++ b/public/docs/ts/latest/guide/router.jade @@ -47,7 +47,7 @@ include ../_util-fns * navigating under [program control](#navigate) * embedding critical information in the URL with [route parameters](#route-parameters) * add [child routes](#child-routing-component) under a feature section - * setting an [index route](#index) as the default + * [redirecting](#redirect) from one route to another * confirming or canceling navigation with [guards](#guards) * [CanActivate](#can-activate-guard) to prevent navigation to a route * [CanDeactivate](#can-deactivate-deactivate) to prevent navigation away from the current route @@ -100,6 +100,9 @@ include ../_util-fns The `RouterConfig` is an array of *routes* that describe how to navigate. Each *Route* maps a URL `path` to a component. + There are no **leading slashes** in our **path**. The router parses and builds the URL for us, + allowing us to use relative and absolute paths when navigating between application views. + The `:id` in the third route is a token for a route parameter. In a URL such as `/hero/42`, "42" is the value of the `id` parameter. The corresponding `HeroDetailComponent` will use that value to find and present the hero whose `id` is 42. @@ -167,7 +170,7 @@ table tr td Route td. - Defines how the router should navigate to a component based on a URL pattern. + Defines how the router should navigate to a component based on a URL pattern. Most routes consist of a path and a component type. tr td RouterOutlet @@ -343,10 +346,10 @@ figure.image-display h4#import Import from the Component Router library :marked - We begin by importing some symbols from the router library. - + We begin by importing some symbols from the router library. + The Component Router is in its own `@angular/router` package. - It's not part of the Angular 2 core. + It's not part of the Angular 2 core. The router is an optional service because not all applications need routing and, depending on your requirements, you may need a different routing library. @@ -371,17 +374,17 @@ h4#define-routes Define routes * *When the application requests navigation to the path `/crisis-center`, create or retrieve an instance of the `CrisisListComponent`, display its view, and update the browser's address location and history with the URL - for that path.* + for that path.* h4#provideRouter Call provideRouter :marked - We pass the route configuration to the `provideRouter` function which returns an array containing the configured + We pass the route configuration to the `provideRouter` function which returns an array containing the configured `Router` service provider ... and some other, unseen providers that the routing library requires. :marked We add the `provideRouter` array to an `APP_ROUTER_PROVIDERS` array and export it. - We could add *additional* service providers to `APP_ROUTER_PROVIDERS` — + We could add *additional* service providers to `APP_ROUTER_PROVIDERS` — providers that are specific to our routing configuration. We don't have any yet. We will have some later in this chapter. @@ -398,10 +401,10 @@ h4#register-providers Register routing in bootstrap and pass it as the second parameter of the `bootstrap` function. +makeExample('router/ts/app/main.1.ts','all', 'main.ts')(format=".") :marked - Providing the router providers at the root makes the Router available everywhere in our application. + Providing the router providers at bootstrap makes the Router available everywhere in our application. .alert.is-important :marked - We must register router providers in `bootstrap`. + We must register router providers in `bootstrap`. We cannot wait to do it in `AppComponent`. h3#shell The AppComponent shell @@ -424,7 +427,7 @@ h3#router-outlet RouterOutlet .l-sub-section :marked A template may hold exactly one ***unnamed*** ``. - The router supports multiple *named* outlets, a feature we'll cover in future. + The router supports multiple *named* outlets, a feature we'll cover in future. h3#router-link RouterLink binding :marked @@ -434,7 +437,7 @@ h3#router-link RouterLink binding The template expression to the right of the equals (=) returns a *link parameters array*. A link parameters array holds the ingredients for router navigation: - * the *path* of the route to the destination component + * the *path* of the route to the destination component * optional route and query parameters that go into the route URL The arrays in this example each have a single string parameter, the path of a route that @@ -445,7 +448,7 @@ h3#router-link RouterLink binding h3#router-directives ROUTER_DIRECTIVES :marked - `RouterLink` and `RouterOutlet` are directives in the `ROUTER_DIRECTIVES` collection. + `RouterLink` and `RouterOutlet` are directives in the `ROUTER_DIRECTIVES` collection. Remember to add them to the `directives` array of the `@Component` metadata. +makeExample('router/ts/app/app.component.1.ts','directives')(format=".") :marked @@ -574,8 +577,8 @@ figure.image-display ### *Hero* feature route configuration - We recommend giving each feature area its own route configuration file. - + We recommend giving each feature area its own route configuration file. + Create a new `hero.routes.ts` in the `heroes` folder like this: +makeExample('router/ts/app/heroes/heroes.routes.ts','', 'app/heroes/heroes.routes.ts')(format=".") :marked @@ -610,7 +613,7 @@ code-example(format="." language="bash"). h3#merge-hero-routes Merge hero routes into application routes :marked - Our application doesn't know about our hero routes yet. + Our application doesn't know about our hero routes yet. We'll need to merge them into the application routes we defined in `app.routes.ts`. Update `app.routes.ts` as follows: @@ -626,7 +629,7 @@ h3#merge-hero-routes Merge hero routes into application routes h3#navigate Navigate to hero detail imperatively :marked - *We won't navigate to the detail component by clicking a link* + *We won't navigate to the detail component by clicking a link* so we won't be adding a new `RouterLink` anchor tag to the shell. Instead, when the user *clicks* a hero in the list, we'll *command* the router @@ -700,7 +703,7 @@ a#hero-detail-ctor .l-sub-section :marked - Learn about the `ngOnInit` and `ngOnDestroy` methods in the + Learn about the `ngOnInit` and `ngOnDestroy` methods in the [Lifecycle Hooks](lifecycle-hooks.html) chapter. h4#reuse Observable params and component re-use @@ -708,11 +711,11 @@ h4#reuse Observable params and component re-use In this example, we subscribe to the route params `Observable`. That implies that the route params can change during the lifetime of this component. - They might. By default, the router reuses a component instance when it re-navigates to the same component type + They might. By default, the router reuses a component instance when it re-navigates to the same component type without visiting a different component first. The parameters can change between each re-use. - Suppose a parent component navigation bar had "forward" and "back" buttons - that scrolled through the list of heroes. + Suppose a parent component navigation bar had "forward" and "back" buttons + that scrolled through the list of heroes. Each click navigated imperatively to the `HeroDetailComponent` with the next or previous `id`. We don't want the router to remove the current `HeroDetailComponent` instance from the @@ -720,13 +723,13 @@ h4#reuse Observable params and component re-use That could be visibly jarring. Better to simply re-use the same component instance and update the parameter. - But `ngOnInit` is only called once per instantiation. + But `ngOnInit` is only called once per instantiation. We need a way to detect when the route parameters change from _within the same instance_. The observable `params` property handles that beautifully. h4#snapshot Snapshot: the no-observable alternative :marked - This application won't reuse the `HeroDetailComponent`. + This application won't reuse the `HeroDetailComponent`. We always return to the hero list to select another hero to view. There's no way to navigate from hero detail to hero detail without visiting the list component in between. @@ -742,8 +745,8 @@ h4#snapshot Snapshot: the no-observable alternative .l-sub-section :marked **Remember:** we only get the _initial_ value of the parameters with this technique. - Stick with the observable `params` approach if there's even a chance that we might navigate - to this component multiple times in a row. + Stick with the observable `params` approach if there's even a chance that we might navigate + to this component multiple times in a row. We are leaving the observable `params` strategy in place just in case. h3#nav-to-list Navigating back to the list component @@ -841,7 +844,7 @@ h3#nav-to-list Navigating back to the list component * Our `CrisisService` is only needed within the *Crisis Center* feature area. We should limit access to it to that feature area. - * Changes to a sub-module such as *Crisis Center* shouldn't provoke changes to the `AppComponent` or + * Changes to a sub-module such as *Crisis Center* shouldn't provoke changes to the `AppComponent` or any other feature's component. We need to [*separate our concerns*](https://blog.8thlight.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html). @@ -861,7 +864,7 @@ h3#nav-to-list Navigating back to the list component * each area with its own area root component * each area root component with its own router-outlet and child routes * area routes rarely (if ever) cross - + If we had many feature areas, their component trees might look like this: figure.image-display @@ -883,8 +886,8 @@ h3#child-routing-component Child Routing Component * It is dead simple — simpler even than the `AppComponent` template. It has no content, no links, just a `` for the *Crisis Center* child views. - Unlike `AppComponent` (and most other components), it **lacks a selector**. - It doesn't need one. We don't *embed* this component in a parent template. + Unlike `AppComponent` (and most other components), it **lacks a selector**. + It doesn't need one. We don't *embed* this component in a parent template. We *navigate* to it from the outside, via the router. .l-sub-section :marked @@ -907,9 +910,9 @@ h3#child-routing-component Child Routing Component First we can evolve the service independently of the rest of the application without fear of breaking what should be unrelated modules. - + Second, we can delay loading this service into memory until we need it. - We can remove it from the application launch bundle, + We can remove it from the application launch bundle, reducing the size of the initial payload and improving performance. We can load it optionally, asynchronously with the other *Crisis Center* components if and when the user begins that workflow. @@ -924,11 +927,11 @@ h3#child-routing-component Child Routing Component We create a `crisis-center.routes.ts` file as we did the `heroes.routes.ts` file. But this time we define **child routes** *within* the parent `/crisis-center` route. -+makeExample('router/ts/app/crisis-center/crisis-center.routes.1.ts', 'routes', 'app/crisis-center/crisis-center.routes.ts (Routes)' )(format='.') ++makeExample('router/ts/app/crisis-center/crisis-center.routes.1.ts', 'routes', 'app/crisis-center/crisis-center.routes.ts (Routes)' )(format='.') :marked Notice that the parent `/crisis-center` route has a `children` property with an array of two routes. - These two routes navigate to the two *Crisis Center* child components, + These two routes navigate to the two *Crisis Center* child components, `CrisisListComponent` and `CrisisDetailComponent`. There are some *important differences* in the treatment of these routes. @@ -939,8 +942,8 @@ h3#child-routing-component Child Routing Component Second, the child paths *extend* the path of their parent route. Normally paths that begin with `/` refer to the root of the application. - Here they are appended to the path to the `CrisisCenterComponent`. - + Here they are appended to the path to the `CrisisCenterComponent`. + To write an URL that navigates to the `CrisisListComponent`, we'd append its child route path, `/`, to `/crisis-center`. @@ -952,7 +955,7 @@ code-example(format=""). :marked Here's the complete `crisis-center.routes.ts` with its imports. -+makeExample('router/ts/app/crisis-center/crisis-center.routes.1.ts', '', 'app/crisis-center/crisis-center.routes.ts' )(format='.') ++makeExample('router/ts/app/crisis-center/crisis-center.routes.1.ts', '', 'app/crisis-center/crisis-center.routes.ts' )(format='.') h3#merge-crisis-routes Merge crisis routes into the application routes :marked @@ -960,10 +963,10 @@ h3#merge-crisis-routes Merge crisis routes into the application routes by merging the crisis routes into the app routes: +makeExample('router/ts/app/app.routes.4.ts', '', 'app/app.routes.ts' )(format='.') :marked - We used the spread operator again (...) to insert the crisis routes array. + We used the spread operator again (...) to insert the crisis routes array. -a#index -h3#default-route Setting default routes +a#redirect +h3#redirect Redirecting routes :marked When the application launches, the initial URL in the browser bar is something like: code-example(format=""). @@ -973,15 +976,24 @@ code-example(format=""). The user must click one of the navigation links to trigger a navigation and display something. We want the application to display the list of crises as it would if we pasted `localhost:3000/crisis-center/` into the address bar. - This is our *default* route. + This is our intended default route. - We can arrange for that behavior in several ways. - One way is to add `index: true` to each route on the path to the default component. + We can arrange for that behavior in several ways. + One way is to use a `redirect` to transparently navigate from one route to another. - In our example, we'll add `index: true` to two routes: - 1. The parent route for the `CrisisCenterComponent` - 1. The child route for the `CrisisListComponent` + In our example, we'll add a route to match our initial URL and redirect to our `crisis-center` route: ++makeExample('router/ts/app/crisis-center/crisis-center.routes.2.ts', 'redirect', 'app/crisis-center/crisis-center.routes.ts (redirect route)' )(format='.') +:marked + Since we only want to redirect when our path specifically matches `''`, we've added an extra configuration + to our route using `terminal: true`. Mainly for redirects, the `terminal` property gives us more control over + when the router should continue matching our URL against our defined routes. + +.l-sub-section + :marked + We'll discuss redirects further in a future update to this chapter. + +:marked The updated route definitions look like this: +makeExample('router/ts/app/crisis-center/crisis-center.routes.2.ts', 'routes', 'app/crisis-center/crisis-center.routes.ts (Routes v.2)' )(format='.') @@ -1006,18 +1018,18 @@ h2#guards Route Guards :marked The guard can also tell the router to navigate elsewhere, effectively canceling the current navigation. :marked - The guard *might* return its boolean answer synchronously. + The guard *might* return its boolean answer synchronously. But in many cases, the guard can't produce an answer synchronously. The guard could ask the user a question, save changes to the server, or fetch fresh data. These are all asynchronous operations. Accordingly, a routing guard can return an `Observable` and the - router will wait for the observable to resolve to `true` or `false. + router will wait for the observable to resolve to `true` or `false. The router supports two kinds of guards: - - 1. [CanActivate](../api/router/index/CanActivate-interface.html) to mediate navigation *to* a route. - + + 1. [CanActivate](../api/router/index/CanActivate-interface.html) to mediate navigation *to* a route. + 2. [CanDeactivate](../api/router/index/CanDeactivate-interface.html) to mediate navigation *away* from the current route. .l-sub-section @@ -1027,7 +1039,7 @@ h2#guards Route Guards We can have multiple guards at every level of a routing hierarchy. The router checks the `CanDeactive` guards first, from deepest child route to the top. Then it checks the `CanActivate` guards from the top down to the deepest child route. - If _any_ guard returns false, pending guards that have not completed will be canceled, + If _any_ guard returns false, pending guards that have not completed will be canceled, and the entire navigation is canceled. Let's look at some examples. @@ -1058,27 +1070,27 @@ h3#can-activate-guard CanActivate: requiring authentication +makeExample('router/ts/app/crisis-center/crisis-center.routes.3.ts', 'admin-route-no-guard', 'crisis-center.routes.ts (admin route)')(format=".") :marked And we add a link to the `AppComponent` shell that users can click to get to this feature. -+makeExample('router/ts/app/app.component.4.ts', 'template', 'app/app.component.ts (template)')(format=".") ++makeExample('router/ts/app/app.component.4.ts', 'template', 'app/app.component.ts (template)')(format=".") :marked #### Guard the admin feature - Currently every route within our *Crisis Center* is open to everyone. + Currently every route within our *Crisis Center* is open to everyone. The new *admin* feature should be accessible only to authenticated users. - We could hide the link until the user logs in. But that's tricky and difficult to maintain. + We could hide the link until the user logs in. But that's tricky and difficult to maintain. Instead we'll write a `CanActivate` guard to redirect anonymous users to the login page when they try to reach the admin component. - + This is a general purpose guard — we can imagine other features that require authenticated users — - so we create an `auth.guard.ts` in the application root folder. - + so we create an `auth.guard.ts` in the application root folder. + At the moment we're interested in seeing how guards work so our first version does nothing useful. It simply logs to console and `returns` true immediately, allowing navigation to proceed: -+makeExample('router/ts/app/auth.guard.1.ts', '', 'app/auth.guard.ts')(format=".") ++makeExample('router/ts/app/auth.guard.1.ts', '', 'app/auth.guard.ts')(format=".") :marked - Next we open `crisis-center.routes.ts `, import the `AuthGuard` class, and + Next we open `crisis-center.routes.ts `, import the `AuthGuard` class, and update the admin route with a `CanActivate` guard property that references it: +makeExample('router/ts/app/crisis-center/crisis-center.routes.ts', 'admin-route', 'crisis-center.routes.ts (guarded admin route)')(format=".") - Our admin feature is now protected by the guard, albeit protected poorly. + Our admin feature is now protected by the guard, albeit protected poorly. :marked #### Teach *AuthGuard* to authenticate Let's make our `AuthGuard` at least pretend to authenticate. @@ -1087,24 +1099,24 @@ h3#can-activate-guard CanActivate: requiring authentication Here's a demo `AuthService`: +makeExample('router/ts/app/auth.service.ts', '', 'app/auth.service.ts')(format=".") :marked - Although it doesn't actually log in, it has what we need for this discussion. + Although it doesn't actually log in, it has what we need for this discussion. It has an `isLoggedIn` flag to tell us whether the user is authenticated. Its `login` method simulates an API call to an external service by returning an observable that resolves successfully after a short pause. Let's revise our `AuthGuard` to call it. +makeExample('router/ts/app/auth.guard.ts', '', 'app/auth.guard.ts (v.2)')(format=".") :marked - Notice that we *inject* the `AuthService` and the `Router` in the constructor. + Notice that we *inject* the `AuthService` and the `Router` in the constructor. We haven't provided the `AuthService` yet but it's good to know that we can inject helpful services into our routing guards. - This guard returns a synchronous boolean result. + This guard returns a synchronous boolean result. If the user is logged in, it returns true and the navigation continues. If the user is not logged in, we tell the router to navigate to a login page — a page we haven't created yet. This secondary navigation automatically cancels the current navigation; we return `false` just to be clear about that. #### Add the *LoginComponent* - We need a `LoginComponent` for the user to log in to the app. + We need a `LoginComponent` for the user to log in to the app. There is nothing new about this component or the way we wire it into the router configuration. Here is the pertinent code, offered without comment: +makeTabs( @@ -1427,7 +1439,7 @@ code-example(format="." language="bash"). :marked ## Wrap Up We've covered a lot of ground in this chapter and the application is too big to reprint here. - Please visit the [live example](/resources/live-examples/router/ts/plnkr.html) and + Please visit the [live example](/resources/live-examples/router/ts/plnkr.html) and where you can download the final source code. .l-main-section diff --git a/public/docs/ts/latest/guide/security.jade b/public/docs/ts/latest/guide/security.jade new file mode 100644 index 0000000000..bfefc79095 --- /dev/null +++ b/public/docs/ts/latest/guide/security.jade @@ -0,0 +1,245 @@ +block includes + include ../_util-fns +:marked + Web application security has many aspects. This documentation describes Angular's built in + protections against common web application vulnerabilities and attacks, such as Cross Site + Scripting Attacks. It does not cover application level security, such as authentication (_Who is + this user?_) or authorization (_What can this user do?_). + + The [Open Web Application Security Project (OWASP)](https://www.owasp.org/index.php/Category:OWASP_Guide_Project) + has further information on the attacks and mitigations described below. + +.l-main-section +:marked + # Table Of Contents + + * [Reporting Vulnerabilities](#report-issues) + * [Best Practices](#best-practices) + * [Preventing Cross-Site Scripting (XSS)](#xss) + * [Trusting Safe Values](#bypass-security-apis) + * [HTTP-level Vulnerabilities](#http) + * [Auditing Angular Applications](#code-review) + +p Try the #[+liveExampleLink2()] of the code shown in this chapter. + +.l-main-section +h2#report-issues Reporting Vulnerabilities +:marked + Email us at [security@angular.io](mailto:security@angular.io) to report vulnerabilities in + Angular itself. + + For further details on how Google handles security issues please refer to [Google's security + philosophy](https://www.google.com/about/appsecurity/). + +.l-main-section +h2#best-practices Best Practices +:marked + * **Keep current with the latest Angular library releases.** + We regularly update our Angular libraries and these updates may fix security defects discovered in + previous version. Check the Angular [change + log](https://github.com/angular/angular/blob/master/CHANGELOG.md) for security-related updates. + + * **Don't modify your copy of Angular.** + Private, customized versions of Angular tend to fall behind the current version and may neglect + important security fixes and enhancements. Instead, share your Angular improvements with the + community and make a pull request. + + * **Avoid Angular APIs marked in the documentation as “[_Security Risk_](#bypass-security-apis)”.** + +.l-main-section +h2#xss Preventing Cross-Site Scripting (XSS) +:marked + [Cross-Site Scripting (XSS)](https://en.wikipedia.org/wiki/Cross-site_scripting) enables attackers + to inject malicious code into web pages. Such code can then for example steal user's data (in + particular their login data), or perform actions impersonating the user. This is one of the most + common attacks on the web. + + To block XSS attacks, we must prevent malicious code from entering the DOM. For example, if an + attacker can trick us into inserting a `