diff --git a/public/docs/_examples/cb-form-validation/e2e-spec.ts b/public/docs/_examples/cb-form-validation/e2e-spec.ts
new file mode 100644
index 0000000000..bbd4a7ff03
--- /dev/null
+++ b/public/docs/_examples/cb-form-validation/e2e-spec.ts
@@ -0,0 +1,180 @@
+///
+'use strict'; // necessary for node!
+
+// THESE TESTS ARE INCOMPLETE
+describeIf(browser.appIsTs || browser.appIsJs, 'Form Validation Tests', function () {
+
+ beforeAll(function () {
+ browser.get('');
+ });
+
+ describe('Hero Form 1', () => {
+ beforeAll(() => {
+ getPage('hero-form-template1');
+ });
+
+ tests();
+ });
+
+ describe('Hero Form 2', () => {
+ beforeAll(() => {
+ getPage('hero-form-template2');
+ });
+
+ tests();
+ bobTests();
+ });
+
+ describe('Hero Form 3 (Reactive)', () => {
+ beforeAll(() => {
+ getPage('hero-form-reactive3');
+ makeNameTooLong();
+ });
+
+ tests();
+ bobTests();
+ });
+});
+
+//////////
+
+const testName = 'Test Name';
+
+let page: {
+ section: protractor.ElementFinder,
+ form: protractor.ElementFinder,
+ title: protractor.ElementFinder,
+ nameInput: protractor.ElementFinder,
+ alterEgoInput: protractor.ElementFinder,
+ powerSelect: protractor.ElementFinder,
+ errorMessages: protractor.ElementArrayFinder,
+ heroFormButtons: protractor.ElementArrayFinder,
+ heroSubmitted: protractor.ElementFinder
+};
+
+function getPage(sectionTag: string) {
+ let section = element(by.css(sectionTag));
+ let buttons = section.all(by.css('button'));
+
+ page = {
+ section: section,
+ form: section.element(by.css('form')),
+ title: section.element(by.css('h1')),
+ nameInput: section.element(by.css('#name')),
+ alterEgoInput: section.element(by.css('#alterEgo')),
+ powerSelect: section.element(by.css('#power')),
+ errorMessages: section.all(by.css('div.alert')),
+ heroFormButtons: buttons,
+ heroSubmitted: section.element(by.css('hero-submitted > div'))
+ };
+}
+
+function tests() {
+ it('should display correct title', function () {
+ expect(page.title.getText()).toContain('Hero Form');
+ });
+
+ it('should not display submitted message before submit', function () {
+ expect(page.heroSubmitted.isElementPresent(by.css('h2'))).toBe(false);
+ });
+
+ it('should have form buttons', function () {
+ expect(page.heroFormButtons.count()).toEqual(2);
+ });
+
+ it('should have error at start', function () {
+ expectFormIsInvalid();
+ });
+
+ // it('showForm', function () {
+ // page.form.getInnerHtml().then(html => console.log(html));
+ // });
+
+ it('should have disabled submit button', function () {
+ expect(page.heroFormButtons.get(0).isEnabled()).toBe(false);
+ });
+
+ it('resetting name to valid name should clear errors', function () {
+ const ele = page.nameInput;
+ expect(ele.isPresent()).toBe(true, 'nameInput should exist');
+ ele.clear();
+ ele.sendKeys(testName);
+ expectFormIsValid();
+ });
+
+ it('should produce "required" error after clearing name', function () {
+ page.nameInput.clear();
+ // page.alterEgoInput.click(); // to blur ... didn't work
+ page.nameInput.sendKeys('x', protractor.Key.BACK_SPACE); // ugh!
+ expect(page.form.getAttribute('class')).toMatch('ng-invalid');
+ expect(page.errorMessages.get(0).getText()).toContain('required');
+ });
+
+ it('should produce "at least 4 characters" error when name="x"', function () {
+ page.nameInput.clear();
+ page.nameInput.sendKeys('x'); // too short
+ expectFormIsInvalid();
+ expect(page.errorMessages.get(0).getText()).toContain('at least 4 characters');
+ });
+
+ it('resetting name to valid name again should clear errors', function () {
+ page.nameInput.sendKeys(testName);
+ expectFormIsValid();
+ });
+
+ it('should have enabled submit button', function () {
+ const submitBtn = page.heroFormButtons.get(0);
+ expect(submitBtn.isEnabled()).toBe(true);
+ });
+
+ it('should hide form after submit', function () {
+ page.heroFormButtons.get(0).click();
+ expect(page.title.isDisplayed()).toBe(false);
+ });
+
+ it('submitted form should be displayed', function () {
+ expect(page.heroSubmitted.isElementPresent(by.css('h2'))).toBe(true);
+ });
+
+ it('submitted form should have new hero name', function () {
+ expect(page.heroSubmitted.getText()).toContain(testName);
+ });
+
+ it('clicking edit button should reveal form again', function () {
+ const editBtn = page.heroSubmitted.element(by.css('button'));
+ editBtn.click();
+ expect(page.heroSubmitted.isElementPresent(by.css('h2')))
+ .toBe(false, 'submitted hidden again');
+ expect(page.title.isDisplayed()).toBe(true, 'can see form title');
+ });
+}
+
+function expectFormIsValid() {
+ expect(page.form.getAttribute('class')).toMatch('ng-valid');
+}
+
+function expectFormIsInvalid() {
+ expect(page.form.getAttribute('class')).toMatch('ng-invalid');
+}
+
+function bobTests() {
+ const emsg = 'Someone named "Bob" cannot be a hero.';
+
+ it('should produce "no bob" error after setting name to "Bobby"', function () {
+ page.nameInput.clear();
+ page.nameInput.sendKeys('Bobby');
+ expectFormIsInvalid();
+ expect(page.errorMessages.get(0).getText()).toBe(emsg);
+ });
+
+ it('should be ok again with valid name', function () {
+ page.nameInput.clear();
+ page.nameInput.sendKeys(testName);
+ expectFormIsValid();
+ });
+}
+
+function makeNameTooLong() {
+ // make the first name invalid
+ page.nameInput.sendKeys('ThisHeroNameHasWayWayTooManyLetters');
+}
diff --git a/public/docs/_examples/cb-form-validation/e2e-spec.ts.disabled b/public/docs/_examples/cb-form-validation/e2e-spec.ts.disabled
deleted file mode 100644
index 60fe5ae0f3..0000000000
--- a/public/docs/_examples/cb-form-validation/e2e-spec.ts.disabled
+++ /dev/null
@@ -1,64 +0,0 @@
-///
-'use strict'; // necessary for node!
-describeIf(browser.appIsTs || browser.appIsJs, 'Forms 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/cb-form-validation/ts/app/reactive/hero-form-reactive.component.ts b/public/docs/_examples/cb-form-validation/ts/app/reactive/hero-form-reactive.component.ts
index f33d37bb13..7e7dfa0739 100644
--- a/public/docs/_examples/cb-form-validation/ts/app/reactive/hero-form-reactive.component.ts
+++ b/public/docs/_examples/cb-form-validation/ts/app/reactive/hero-form-reactive.component.ts
@@ -38,7 +38,6 @@ export class HeroFormReactiveComponent implements OnInit {
addHero() {
this.hero = new Hero(42, '', '');
this.buildForm();
- this.onValueChanged();
// #enddocregion add-hero
// #enddocregion class
@@ -74,17 +73,20 @@ export class HeroFormReactiveComponent implements OnInit {
this.heroForm.valueChanges
.subscribe(data => this.onValueChanged(data));
+
+ this.onValueChanged(); // (re)set validation messages now
}
// #enddocregion form-builder
onValueChanged(data?: any) {
- const controls = this.heroForm ? this.heroForm.controls : {};
+ if (!this.heroForm) { return; }
+ const form = this.heroForm;
for (const field in this.formErrors) {
// clear previous error message (if any)
this.formErrors[field] = '';
- const control = controls[field];
+ const control = form.get(field);
if (control && control.dirty && !control.valid) {
const messages = this.validationMessages[field];
diff --git a/public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template2.component.ts b/public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template2.component.ts
index ae6c0367b4..e2781ec617 100644
--- a/public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template2.component.ts
+++ b/public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template2.component.ts
@@ -60,12 +60,13 @@ export class HeroFormTemplate2Component implements AfterViewChecked {
// #docregion handler
onValueChanged(data?: any) {
- const controls = this.heroForm ? this.heroForm.controls : {};
+ if (!this.heroForm) { return; }
+ const form = this.heroForm.form;
for (const field in this.formErrors) {
// clear previous error message (if any)
this.formErrors[field] = '';
- const control = controls[field];
+ const control = form.get(field);
if (control && control.dirty && !control.valid) {
const messages = this.validationMessages[field];
diff --git a/public/docs/_examples/cb-form-validation/ts/forms.css b/public/docs/_examples/cb-form-validation/ts/forms.css
index e95fd4bf8d..67ad13037b 100644
--- a/public/docs/_examples/cb-form-validation/ts/forms.css
+++ b/public/docs/_examples/cb-form-validation/ts/forms.css
@@ -1,11 +1,7 @@
-.ng-valid[required] {
+.ng-valid[required], .ng-valid.required {
border-left: 5px solid #42A948; /* green */
}
-.ng-invalid {
+.ng-invalid:not(form) {
border-left: 5px solid #a94442; /* red */
}
-
-.ng-valid.required {
- border-left: 5px solid #42A948; /* green */
-}
\ No newline at end of file
diff --git a/public/docs/_examples/forms/ts/forms.css b/public/docs/_examples/forms/ts/forms.css
index d7e11405b1..13ffbe1203 100644
--- a/public/docs/_examples/forms/ts/forms.css
+++ b/public/docs/_examples/forms/ts/forms.css
@@ -1,9 +1,9 @@
/* #docregion */
-.ng-valid[required] {
+.ng-valid[required], .ng-valid.required {
border-left: 5px solid #42A948; /* green */
}
-.ng-invalid {
+.ng-invalid:not(form) {
border-left: 5px solid #a94442; /* red */
}
-/* #enddocregion */
\ No newline at end of file
+/* #enddocregion */
diff --git a/public/docs/ts/latest/cookbook/form-validation.jade b/public/docs/ts/latest/cookbook/form-validation.jade
index 877026adcc..d9ae352ce5 100644
--- a/public/docs/ts/latest/cookbook/form-validation.jade
+++ b/public/docs/ts/latest/cookbook/form-validation.jade
@@ -327,7 +327,9 @@ a#reactive
A real app would retrieve the hero asynchronously from a data service, a task best performed in the `ngOnInit` hook.
:marked
- the `buildForm` method uses the `FormBuilder` (`fb`) to declare the form control model.
- Then it attaches the same `onValueChanged` handler to the form.
+ Then it attaches the same `onValueChanged` handler (there's a one line difference)
+ to the form's `valueChanged` event and calls it immediately
+ to set error messages for the new control model.
:marked
#### _FormBuilder_ declaration
@@ -371,9 +373,6 @@ a#reactive
Then it calls `buildForm` again which replaces the previous `heroForm` control model with a new one.
The `