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 `
` tag's `[formGroup]` binding refreshes the page with the new control model. - Finally, it calls the `onValueChanged` handler to clear previous error messages and reset them - to reflect Angular's validation of the new `hero` object. - Here's the complete reactive component file, compared to the two template-driven component files. +makeTabs( `cb-form-validation/ts/app/reactive/hero-form-reactive.component.ts,