From 452c16dce2ba417487e17e537db9c56d617a9e30 Mon Sep 17 00:00:00 2001 From: Kapunahele Wong Date: Tue, 31 Jan 2017 20:36:32 -0500 Subject: [PATCH] docs(reactive-forms): add reactive forms guide (#2835) --- .../forms/ts/app/hero-form.component.html | 8 +- .../docs/_examples/reactive-forms/e2e-spec.ts | 1020 ++++++++++++++++ .../reactive-forms/ts/app/app.component.1.ts | 13 + .../reactive-forms/ts/app/app.component.ts | 13 + .../reactive-forms/ts/app/app.module.ts | 39 + .../reactive-forms/ts/app/data-model.ts | 40 + .../reactive-forms/ts/app/demo.component.html | 40 + .../reactive-forms/ts/app/demo.component.ts | 48 + .../reactive-forms/ts/app/demo.module.ts | 33 + .../ts/app/hero-detail-1.component.html | 8 + .../ts/app/hero-detail-1.component.ts | 15 + .../ts/app/hero-detail-2.component.html | 18 + .../ts/app/hero-detail-2.component.ts | 18 + .../ts/app/hero-detail-3.component.html | 16 + .../ts/app/hero-detail-3.component.ts | 28 + .../ts/app/hero-detail-3a.component.ts | 26 + .../ts/app/hero-detail-4.component.html | 46 + .../ts/app/hero-detail-4.component.ts | 35 + .../ts/app/hero-detail-5.component.html | 56 + .../ts/app/hero-detail-5.component.ts | 36 + .../ts/app/hero-detail-6.component.html | 46 + .../ts/app/hero-detail-6.component.ts | 59 + .../ts/app/hero-detail-7.component.html | 46 + .../ts/app/hero-detail-7.component.ts | 67 ++ .../ts/app/hero-detail-8.component.html | 70 ++ .../ts/app/hero-detail-8.component.ts | 69 ++ .../ts/app/hero-detail.component.html | 73 ++ .../ts/app/hero-detail.component.ts | 108 ++ .../ts/app/hero-list.component.1.html | 8 + .../ts/app/hero-list.component.html | 17 + .../ts/app/hero-list.component.ts | 32 + .../reactive-forms/ts/app/hero.service.ts | 26 + .../reactive-forms/ts/app/main-final.ts | 5 + .../_examples/reactive-forms/ts/app/main.ts | 6 + .../reactive-forms/ts/example-config.json | 0 .../reactive-forms/ts/final.plnkr.json | 20 + .../reactive-forms/ts/index-final.html | 31 + .../_examples/reactive-forms/ts/index.html | 31 + .../_examples/reactive-forms/ts/plnkr.json | 14 + public/docs/ts/latest/guide/_data.json | 5 + public/docs/ts/latest/guide/change-log.jade | 8 + public/docs/ts/latest/guide/forms.jade | 2 +- .../docs/ts/latest/guide/reactive-forms.jade | 1045 +++++++++++++++++ .../devguide/reactive-forms/address-group.png | Bin 0 -> 4574 bytes .../reactive-forms/addresses-array.png | Bin 0 -> 3266 bytes .../devguide/reactive-forms/hero-detail.png | Bin 0 -> 4608 bytes .../devguide/reactive-forms/hero-list.png | Bin 0 -> 5950 bytes .../devguide/reactive-forms/json-output.png | Bin 0 -> 39964 bytes .../reactive-forms/just-formcontrol.png | Bin 0 -> 26486 bytes .../reactive-forms/save-revert-buttons.png | Bin 0 -> 7023 bytes .../reactive-forms/validators-json-output.png | Bin 0 -> 60846 bytes .../reactive-forms/ts/final-eplnkr.html | 948 +++++++++++++++ .../reactive-forms/ts/final-plnkr.html | 948 +++++++++++++++ 53 files changed, 5235 insertions(+), 5 deletions(-) create mode 100644 public/docs/_examples/reactive-forms/e2e-spec.ts create mode 100644 public/docs/_examples/reactive-forms/ts/app/app.component.1.ts create mode 100644 public/docs/_examples/reactive-forms/ts/app/app.component.ts create mode 100644 public/docs/_examples/reactive-forms/ts/app/app.module.ts create mode 100644 public/docs/_examples/reactive-forms/ts/app/data-model.ts create mode 100644 public/docs/_examples/reactive-forms/ts/app/demo.component.html create mode 100644 public/docs/_examples/reactive-forms/ts/app/demo.component.ts create mode 100644 public/docs/_examples/reactive-forms/ts/app/demo.module.ts create mode 100644 public/docs/_examples/reactive-forms/ts/app/hero-detail-1.component.html create mode 100644 public/docs/_examples/reactive-forms/ts/app/hero-detail-1.component.ts create mode 100644 public/docs/_examples/reactive-forms/ts/app/hero-detail-2.component.html create mode 100644 public/docs/_examples/reactive-forms/ts/app/hero-detail-2.component.ts create mode 100644 public/docs/_examples/reactive-forms/ts/app/hero-detail-3.component.html create mode 100644 public/docs/_examples/reactive-forms/ts/app/hero-detail-3.component.ts create mode 100644 public/docs/_examples/reactive-forms/ts/app/hero-detail-3a.component.ts create mode 100644 public/docs/_examples/reactive-forms/ts/app/hero-detail-4.component.html create mode 100644 public/docs/_examples/reactive-forms/ts/app/hero-detail-4.component.ts create mode 100644 public/docs/_examples/reactive-forms/ts/app/hero-detail-5.component.html create mode 100644 public/docs/_examples/reactive-forms/ts/app/hero-detail-5.component.ts create mode 100644 public/docs/_examples/reactive-forms/ts/app/hero-detail-6.component.html create mode 100644 public/docs/_examples/reactive-forms/ts/app/hero-detail-6.component.ts create mode 100644 public/docs/_examples/reactive-forms/ts/app/hero-detail-7.component.html create mode 100644 public/docs/_examples/reactive-forms/ts/app/hero-detail-7.component.ts create mode 100644 public/docs/_examples/reactive-forms/ts/app/hero-detail-8.component.html create mode 100644 public/docs/_examples/reactive-forms/ts/app/hero-detail-8.component.ts create mode 100644 public/docs/_examples/reactive-forms/ts/app/hero-detail.component.html create mode 100644 public/docs/_examples/reactive-forms/ts/app/hero-detail.component.ts create mode 100644 public/docs/_examples/reactive-forms/ts/app/hero-list.component.1.html create mode 100644 public/docs/_examples/reactive-forms/ts/app/hero-list.component.html create mode 100644 public/docs/_examples/reactive-forms/ts/app/hero-list.component.ts create mode 100644 public/docs/_examples/reactive-forms/ts/app/hero.service.ts create mode 100644 public/docs/_examples/reactive-forms/ts/app/main-final.ts create mode 100644 public/docs/_examples/reactive-forms/ts/app/main.ts create mode 100644 public/docs/_examples/reactive-forms/ts/example-config.json create mode 100644 public/docs/_examples/reactive-forms/ts/final.plnkr.json create mode 100644 public/docs/_examples/reactive-forms/ts/index-final.html create mode 100644 public/docs/_examples/reactive-forms/ts/index.html create mode 100644 public/docs/_examples/reactive-forms/ts/plnkr.json create mode 100644 public/docs/ts/latest/guide/reactive-forms.jade create mode 100644 public/resources/images/devguide/reactive-forms/address-group.png create mode 100644 public/resources/images/devguide/reactive-forms/addresses-array.png create mode 100644 public/resources/images/devguide/reactive-forms/hero-detail.png create mode 100644 public/resources/images/devguide/reactive-forms/hero-list.png create mode 100644 public/resources/images/devguide/reactive-forms/json-output.png create mode 100644 public/resources/images/devguide/reactive-forms/just-formcontrol.png create mode 100644 public/resources/images/devguide/reactive-forms/save-revert-buttons.png create mode 100644 public/resources/images/devguide/reactive-forms/validators-json-output.png create mode 100644 public/resources/live-examples/reactive-forms/ts/final-eplnkr.html create mode 100644 public/resources/live-examples/reactive-forms/ts/final-plnkr.html 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 a44c9e9ee1..bf02bb195e 100644 --- a/public/docs/_examples/forms/ts/app/hero-form.component.html +++ b/public/docs/_examples/forms/ts/app/hero-form.component.html @@ -43,7 +43,7 @@ - + @@ -83,7 +83,7 @@
{{ model.power }}

- + @@ -137,7 +137,7 @@ - + @@ -175,7 +175,7 @@ - + diff --git a/public/docs/_examples/reactive-forms/e2e-spec.ts b/public/docs/_examples/reactive-forms/e2e-spec.ts new file mode 100644 index 0000000000..bb788397ef --- /dev/null +++ b/public/docs/_examples/reactive-forms/e2e-spec.ts @@ -0,0 +1,1020 @@ +'use strict'; // necessary for es6 output in node + +import { browser, element, by } from 'protractor'; + +function finalDemoAddressForm(element: any, index: number) { + let form = { + street: element.all(by.css('input[formcontrolname=street]')).get(index).getAttribute('value'), + city: element.all(by.css('input[formcontrolname=city]')).get(index).getAttribute('value'), + state: element.all(by.css('select[formcontrolname=state]')).get(index).getAttribute('value'), + zip: element.all(by.css('input[formcontrolname=zip]')).get(index).getAttribute('value') + }; + return form; +} + +describe('Reactive forms', function() { + let select: any; + + beforeEach(function() { + browser.get(''); + select = element(by.css('.container > h4 > select')); + }); + + describe('navigation', function() { + it('should display the title', function() { + let title = element(by.css('.container > h1')); + expect(title.getText()).toBe('Reactive Forms'); + }); + + it('should contain a dropdown with each example', function() { + expect(select.isDisplayed()).toBe(true); + }); + + it('should have 9 options for different demos', function() { + let options = select.all(by.tagName('option')); + expect(options.count()).toBe(9); + }); + + it('should start with Final Demo', function() { + select.getAttribute('value').then(function(demo: string) { + expect(demo).toBe('Final Demo'); + }); + }); + }); + +// *************Begin Final Demo test******************************* + + describe('final demo', function() { + it('does not select any hero by default', function() { + let heroSection = element(by.css('hero-list > div')); + expect(heroSection.isPresent()).toBe(false); + }); + + it('refreshes the page upon button click', function() { + // We move to another page... + let whirlwindButton = element.all(by.css('nav a')).get(0); + whirlwindButton.click(); + let refresh = element(by.css('button')); + refresh.click(); + let heroSection = element(by.css('hero-list > div')); + expect(heroSection.isPresent()).toBe(false); + }); + + describe('Whirlwind form', function() { + beforeEach(function() { + let whirlwindButton = element.all(by.css('nav a')).get(0); + whirlwindButton.click(); + }); + + it('should show a hero information when the button is clicked', function() { + let editMessage = element(by.css('hero-list > div > h3')); + expect(editMessage.getText()).toBe('Editing: Whirlwind'); + }); + + it('should show a form with the selected hero information', function() { + let nameInput = element(by.css('input[formcontrolname=name]')); + expect(nameInput.getAttribute('value')).toBe('Whirlwind'); + let address1 = finalDemoAddressForm(element, 0); + expect(address1.street).toBe('123 Main'); + expect(address1.state).toBe('CA'); + expect(address1.zip).toBe('94801'); + expect(address1.city).toBe('Anywhere'); + let address2 = finalDemoAddressForm(element, 1); + expect(address2.street).toBe('456 Maple'); + expect(address2.state).toBe('VA'); + expect(address2.zip).toBe('23226'); + expect(address2.city).toBe('Somewhere'); + }); + + it('shows a json output from the form', function() { + let json = element(by.css('hero-detail > p')); + expect(json.getText()).toContain('Whirlwind'); + expect(json.getText()).toContain('Anywhere'); + expect(json.getText()).toContain('Somewhere'); + expect(json.getText()).toContain('VA'); + }); + + it('has two disabled buttons by default', function() { + let buttons = element.all(by.css('hero-detail > form > div > button')); + expect(buttons.get(0).getAttribute('disabled')).toBe('true'); + expect(buttons.get(1).getAttribute('disabled')).toBe('true'); + }); + + it('enables the buttons after we edit the form', function() { + let nameInput = element(by.css('input[formcontrolname=name]')); + nameInput.sendKeys('a'); + let buttons = element.all(by.css('hero-detail > form > div > button')); + expect(buttons.get(0).getAttribute('disabled')).toBeNull(); + expect(buttons.get(1).getAttribute('disabled')).toBeNull(); + }); + + it('saves the changes when the save button is clicked', function() { + let nameInput = element(by.css('input[formcontrolname=name]')); + nameInput.sendKeys('a'); + let save = element.all(by.css('hero-detail > form > div > button')).get(0); + save.click(); + let editMessage = element(by.css('hero-list > div > h3')); + expect(editMessage.getText()).toBe('Editing: Whirlwinda'); + }); + + it('reverts the changes when the revert button is clicked', function() { + let nameInput = element(by.css('input[formcontrolname=name]')); + nameInput.sendKeys('a'); + let revert = element.all(by.css('hero-detail > form > div > button')).get(1); + revert.click(); + let editMessage = element(by.css('hero-list > div > h3')); + expect(editMessage.getText()).toBe('Editing: Whirlwind'); + expect(nameInput.getAttribute('value')).toBe('Whirlwind'); + }); + + it('is able to add a new empty address', function() { + let newLairButton = element.all(by.css('button')).get(3); + newLairButton.click(); + let address3 = finalDemoAddressForm(element, 2); + expect(address3.street).toBe(''); + expect(address3.state).toBe(''); + expect(address3.zip).toBe(''); + expect(address3.city).toBe(''); + }); + + it('should show three radio buttons', function() { + let radioButtons = element.all(by.css('input[formcontrolname=power]')); + expect(radioButtons.get(0).getAttribute('value')).toBe('flight'); + expect(radioButtons.get(1).getAttribute('value')).toBe('x-ray vision'); + expect(radioButtons.get(2).getAttribute('value')).toBe('strength'); + }); + + it('should show a checkbox', function() { + let checkbox = element(by.css('input[formcontrolname=sidekick]')); + expect(checkbox.getAttribute('checked')).toBe(null); + }); + }); + + describe('Bombastic form', function() { + beforeEach(function() { + let bombastaButton = element.all(by.css('nav a')).get(1); + bombastaButton.click(); + }); + + it('should show a hero information when the button is clicked', function() { + let editMessage = element(by.css('hero-list > div > h3')); + expect(editMessage.getText()).toBe('Editing: Bombastic'); + }); + + it('should show a form with the selected hero information', function() { + let nameInput = element(by.css('input[formcontrolname=name]')); + expect(nameInput.getAttribute('value')).toBe('Bombastic'); + let address1 = finalDemoAddressForm(element, 0); + expect(address1.street).toBe('789 Elm'); + // expect(address1.state).toBe('OH'); + expect(address1.zip).toBe('04501'); + expect(address1.city).toBe('Smallville'); + }); + + it('shows a json output from the form', function() { + let json = element(by.css('hero-detail > p')); + expect(json.getText()).toContain('Bombastic'); + expect(json.getText()).toContain('Smallville'); + expect(json.getText()).toContain('OH'); + expect(json.getText()).toContain('04501'); + }); + + it('has two disabled buttons by default', function() { + let buttons = element.all(by.css('hero-detail > form > div > button')); + expect(buttons.get(0).getAttribute('disabled')).toBe('true'); + expect(buttons.get(1).getAttribute('disabled')).toBe('true'); + }); + + it('enables the buttons after we edit the form', function() { + let nameInput = element(by.css('input[formcontrolname=name]')); + nameInput.sendKeys('a'); + let buttons = element.all(by.css('hero-detail > form > div > button')); + expect(buttons.get(0).getAttribute('disabled')).toBeNull(); + expect(buttons.get(1).getAttribute('disabled')).toBeNull(); + }); + + it('saves the changes when the save button is clicked', function() { + let nameInput = element(by.css('input[formcontrolname=name]')); + nameInput.sendKeys('a'); + let save = element.all(by.css('hero-detail > form > div > button')).get(0); + save.click(); + let editMessage = element(by.css('hero-list > div > h3')); + expect(editMessage.getText()).toBe('Editing: Bombastica'); + }); + + it('reverts the changes when the revert button is clicked', function() { + let nameInput = element(by.css('input[formcontrolname=name]')); + nameInput.sendKeys('a'); + let revert = element.all(by.css('hero-detail > form > div > button')).get(1); + revert.click(); + let editMessage = element(by.css('hero-list > div > h3')); + expect(editMessage.getText()).toBe('Editing: Bombastic'); + expect(nameInput.getAttribute('value')).toBe('Bombastic'); + }); + + it('is able to add a new empty address', function() { + let newLairButton = element.all(by.css('button')).get(3); + newLairButton.click(); + let address2 = finalDemoAddressForm(element, 1); + expect(address2.street).toBe(''); + expect(address2.state).toBe(''); + expect(address2.zip).toBe(''); + expect(address2.city).toBe(''); + }); + + it('should show three radio buttons', function() { + let radioButtons = element.all(by.css('input[formcontrolname=power]')); + expect(radioButtons.get(0).getAttribute('value')).toBe('flight'); + expect(radioButtons.get(1).getAttribute('value')).toBe('x-ray vision'); + expect(radioButtons.get(2).getAttribute('value')).toBe('strength'); + }); + + it('should show a checbox', function() { + let checkbox = element(by.css('input[formcontrolname=sidekick]')); + expect(checkbox.getAttribute('checked')).toBe(null); + }); + }); + + describe('Magneta form', function() { + + beforeEach(function() { + let magnetaButton = element.all(by.css('nav a')).get(2); + magnetaButton.click(); + }); + + it('should show hero information when the button is clicked', function() { + let editMessage = element(by.css('hero-list > div > h3')); + expect(editMessage.getText()).toBe('Editing: Magneta'); + }); + + it('should show a form with the selected hero information', function() { + let nameInput = element(by.css('input[formcontrolname=name]')); + expect(nameInput.getAttribute('value')).toBe('Magneta'); + }); + + it('shows a json output from the form', function() { + let json = element(by.css('hero-detail > p')); + expect(json.getText()).toContain('Magneta'); + }); + + it('has two disabled buttons by default', function() { + let buttons = element.all(by.css('hero-detail > form > div > button')); + expect(buttons.get(0).getAttribute('disabled')).toBe('true'); + expect(buttons.get(1).getAttribute('disabled')).toBe('true'); + }); + + it('enables the buttons after we edit the form', function() { + let nameInput = element(by.css('input[formcontrolname=name]')); + nameInput.sendKeys('a'); + let buttons = element.all(by.css('hero-detail > form > div > button')); + expect(buttons.get(0).getAttribute('disabled')).toBeNull(); + expect(buttons.get(1).getAttribute('disabled')).toBeNull(); + }); + + it('saves the changes when the save button is clicked', function() { + let nameInput = element(by.css('input[formcontrolname=name]')); + nameInput.sendKeys('a'); + let save = element.all(by.css('hero-detail > form > div > button')).get(0); + save.click(); + let editMessage = element(by.css('hero-list > div > h3')); + expect(editMessage.getText()).toBe('Editing: Magnetaa'); + }); + + it('reverts the changes when the revert button is clicked', function() { + let nameInput = element(by.css('input[formcontrolname=name]')); + nameInput.sendKeys('a'); + let revert = element.all(by.css('hero-detail > form > div > button')).get(1); + revert.click(); + let editMessage = element(by.css('hero-list > div > h3')); + expect(editMessage.getText()).toBe('Editing: Magneta'); + expect(nameInput.getAttribute('value')).toBe('Magneta'); + }); + + it('is able to add a new empty address', function() { + let newLairButton = element.all(by.css('button')).get(3); + newLairButton.click(); + let address = finalDemoAddressForm(element, 0); + expect(address.street).toBe(''); + expect(address.state).toBe(''); + expect(address.zip).toBe(''); + expect(address.city).toBe(''); + }); + + it('should show three radio buttons', function() { + let radioButtons = element.all(by.css('input[formcontrolname=power]')); + expect(radioButtons.get(0).getAttribute('value')).toBe('flight'); + expect(radioButtons.get(1).getAttribute('value')).toBe('x-ray vision'); + expect(radioButtons.get(2).getAttribute('value')).toBe('strength'); + }); + + it('should show a checkbox', function() { + let checkbox = element(by.css('input[formcontrolname=sidekick]')); + expect(checkbox.getAttribute('checked')).toBe(null); + }); + }); + }); // final demo + +// *************Begin FormArray Demo test******************************* + + + describe('formArray demo', function() { + beforeEach(function() { + let FormArrayOption = element.all(by.css('select option')).get(7); + FormArrayOption.click(); + }); + + it('should show FormArray Demo', function() { + select.getAttribute('value').then(function(demo: string) { + expect(demo).toBe('FormArray Demo'); + }); + }); + + it('does not select any hero by default', function() { + let heroSection = element(by.css('hero-list > div')); + expect(heroSection.isPresent()).toBe(false); + }); + + it('refreshes the page upon button click', function() { + // We move to another page... + let whirlwindButton = element.all(by.css('nav a')).get(0); + whirlwindButton.click(); + let refresh = element(by.css('button')); + refresh.click(); + let heroSection = element(by.css('hero-list > div')); + expect(heroSection.isPresent()).toBe(false); + }); + + describe('Whirlwind form', function() { + beforeEach(function() { + let whirlwindButton = element.all(by.css('nav a')).get(0); + whirlwindButton.click(); + }); + + it('should show hero information when the button is clicked', function() { + let editMessage = element(by.css('div.demo > div > div > h3')); + expect(editMessage.getText()).toBe('Editing: Whirlwind'); + }); + + it('should show a form with the selected hero information', function() { + let nameInput = element(by.css('input[formcontrolname=name]')); + expect(nameInput.getAttribute('value')).toBe('Whirlwind'); + let address1 = finalDemoAddressForm(element, 0); + expect(address1.street).toBe('123 Main'); + expect(address1.state).toBe('CA'); + expect(address1.zip).toBe('94801'); + expect(address1.city).toBe('Anywhere'); + let address2 = finalDemoAddressForm(element, 1); + expect(address2.street).toBe('456 Maple'); + expect(address2.state).toBe('VA'); + expect(address2.zip).toBe('23226'); + expect(address2.city).toBe('Somewhere'); + }); + it('shows a json output from the form', function() { + let json = element(by.css('hero-detail-8 > p')); + expect(json.getText()).toContain('Whirlwind'); + expect(json.getText()).toContain('Anywhere'); + expect(json.getText()).toContain('Somewhere'); + expect(json.getText()).toContain('VA'); + }); + + it('is able to add a new empty address', function() { + let newLairButton = element.all(by.css('button')).get(1); + newLairButton.click(); + let address2 = finalDemoAddressForm(element, 2); + expect(address2.street).toBe(''); + expect(address2.state).toBe(''); + expect(address2.zip).toBe(''); + expect(address2.city).toBe(''); + }); + + it('should show three radio buttons', function() { + let radioButtons = element.all(by.css('input[formcontrolname=power]')); + expect(radioButtons.get(0).getAttribute('value')).toBe('flight'); + expect(radioButtons.get(1).getAttribute('value')).toBe('x-ray vision'); + expect(radioButtons.get(2).getAttribute('value')).toBe('strength'); + }); + + it('should show a checkbox', function() { + let checkbox = element(by.css('input[formcontrolname=sidekick]')); + expect(checkbox.getAttribute('checked')).toBe(null); + }); + + }); // Whirlwind form + + describe('Bombastic FormArray form', function() { + beforeEach(function() { + let bombasticButton = element.all(by.css('nav a')).get(1); + bombasticButton.click(); + }); + + it('should show a hero information when the button is clicked', function() { + let editMessage = element(by.css('div.demo > div > div > h3')); + expect(editMessage.getText()).toBe('Editing: Bombastic'); + }); + + it('should show a form with the selected hero information', function() { + let nameInput = element(by.css('input[formcontrolname=name]')); + // nameInput.getAttribute('value').then(function(name: string) { + // expect(name).toBe('Whirlwind'); + // }); + expect(nameInput.getAttribute('value')).toBe('Bombastic'); + let address1 = finalDemoAddressForm(element, 0); + expect(address1.street).toBe('789 Elm'); + // expect(address1.state).toBe('OH'); + // This select should be OH not CA, which it shows in the UI, the JSON shows OH. + expect(address1.zip).toBe('04501'); + expect(address1.city).toBe('Smallville'); + }); + + it('shows a json output from the form', function() { + let json = element(by.css('hero-detail-8 > p')); + expect(json.getText()).toContain('Bombastic'); + expect(json.getText()).toContain('Smallville'); + expect(json.getText()).toContain('04501'); + expect(json.getText()).toContain('789 Elm'); + }); + + it('is able to add a new empty address', function() { + let newLairButton = element.all(by.css('button')).get(1); + newLairButton.click(); + let address1 = finalDemoAddressForm(element, 1); + expect(address1.street).toBe(''); + expect(address1.state).toBe(''); + expect(address1.zip).toBe(''); + expect(address1.city).toBe(''); + }); + + it('should show three radio buttons', function() { + let radioButtons = element.all(by.css('input[formcontrolname=power]')); + expect(radioButtons.get(0).getAttribute('value')).toBe('flight'); + expect(radioButtons.get(1).getAttribute('value')).toBe('x-ray vision'); + expect(radioButtons.get(2).getAttribute('value')).toBe('strength'); + }); + + it('should show a checkbox', function() { + let checkbox = element(by.css('input[formcontrolname=sidekick]')); + expect(checkbox.getAttribute('checked')).toBe(null); + }); + + }); // Bombastic FormArray form + + describe('Magneta FormArray form', function() { + beforeEach(function() { + let magnetaButton = element.all(by.css('nav a')).get(2); + magnetaButton.click(); + }); + + it('should show a hero information when the button is clicked', function() { + let editMessage = element(by.css('div.demo > div > div > h3')); + expect(editMessage.getText()).toBe('Editing: Magneta'); + }); + + it('should show a form with the selected hero information', function() { + let nameInput = element(by.css('input[formcontrolname=name]')); + expect(nameInput.getAttribute('value')).toBe('Magneta'); + }); + + it('shows a json output from the form', function() { + let json = element(by.css('hero-detail-8 > p')); + expect(json.getText()).toContain('Magneta'); + }); + + it('is able to add a new empty address', function() { + let newLairButton = element.all(by.css('button')).get(1); + newLairButton.click(); + let address1 = finalDemoAddressForm(element, 0); + expect(address1.street).toBe(''); + expect(address1.state).toBe(''); + expect(address1.zip).toBe(''); + expect(address1.city).toBe(''); + }); + + it('should show three radio buttons', function() { + let radioButtons = element.all(by.css('input[formcontrolname=power]')); + expect(radioButtons.get(0).getAttribute('value')).toBe('flight'); + expect(radioButtons.get(1).getAttribute('value')).toBe('x-ray vision'); + expect(radioButtons.get(2).getAttribute('value')).toBe('strength'); + }); + + it('should show a checkbox', function() { + let checkbox = element(by.css('input[formcontrolname=sidekick]')); + expect(checkbox.getAttribute('checked')).toBe(null); + }); + + }); // Magneta FormArray form + + }); // formArray demo + + +// *************Begin SetValue Demo test******************************* + + describe('SetValue demo', function() { + beforeEach(function() { + let SetValueOption = element.all(by.css('select option')).get(6); + SetValueOption.click(); + }); + + it('should show SetValue Demo', function() { + select.getAttribute('value').then(function(demo: string) { + expect(demo).toBe('SetValue Demo'); + }); + }); + + it('does not select any hero by default', function() { + let heroSection = element(by.css('hero-list > div')); + expect(heroSection.isPresent()).toBe(false); + }); + + it('refreshes the page upon button click', function() { + // We move to another page... + let whirlwindButton = element.all(by.css('nav a')).get(0); + whirlwindButton.click(); + let refresh = element(by.css('button')); + refresh.click(); + let heroSection = element(by.css('hero-list > div')); + expect(heroSection.isPresent()).toBe(false); + }); + + describe('Whirlwind setValue form', function() { + beforeEach(function() { + let whirlwindButton = element.all(by.css('nav a')).get(0); + whirlwindButton.click(); + }); + + it('should show a hero information when the button is clicked', function() { + let editMessage = element(by.css('.demo > div > div > h3')); + expect(editMessage.getText()).toBe('Editing: Whirlwind'); + }); + + it('should show a form with the selected hero information', function() { + let nameInput = element(by.css('input[formcontrolname=name]')); + expect(nameInput.getAttribute('value')).toBe('Whirlwind'); + let address1 = finalDemoAddressForm(element, 0); + expect(address1.street).toBe('123 Main'); + expect(address1.state).toBe('CA'); + expect(address1.zip).toBe('94801'); + expect(address1.city).toBe('Anywhere'); + }); + + it('shows a json output from the form', function() { + let json = element(by.css('hero-detail-7 > p')); + expect(json.getText()).toContain('Whirlwind'); + expect(json.getText()).toContain('Anywhere'); + let nameOutput = element(by.css('hero-detail-7 > p ~ p')); + expect(nameOutput.getText()).toContain('Name value: Whirlwind'); + let streetOutput = element(by.css('hero-detail-7 > p ~ p ~ p')); + expect(streetOutput.getText()).toContain('Street value: 123 Main'); + }); + + it('should show three radio buttons', function() { + let radioButtons = element.all(by.css('input[formcontrolname=power]')); + expect(radioButtons.get(0).getAttribute('value')).toBe('flight'); + expect(radioButtons.get(1).getAttribute('value')).toBe('x-ray vision'); + expect(radioButtons.get(2).getAttribute('value')).toBe('strength'); + }); + + it('should show a checkbox', function() { + let checkbox = element(by.css('input[formcontrolname=sidekick]')); + expect(checkbox.getAttribute('checked')).toBe(null); + }); + + }); // Whirlwind setValue form + + describe('Bombastic setValue form', function() { + beforeEach(function() { + let bombasticButton = element.all(by.css('nav a')).get(1); + bombasticButton.click(); + }); + + it('should show a hero information when the button is clicked', function() { + let editMessage = element(by.css('.demo > div > div > h3')); + expect(editMessage.getText()).toBe('Editing: Bombastic'); + }); + + it('should show a form with the selected hero information', function() { + let nameInput = element(by.css('input[formcontrolname=name]')); + expect(nameInput.getAttribute('value')).toBe('Bombastic'); + let address1 = finalDemoAddressForm(element, 0); + expect(address1.street).toBe('789 Elm'); + expect(address1.state).toBe('OH'); + expect(address1.zip).toBe('04501'); + expect(address1.city).toBe('Smallville'); + }); + + it('shows a json output from the form', function() { + let json = element(by.css('hero-detail-7 > p')); + expect(json.getText()).toContain('Bombastic'); + expect(json.getText()).toContain('Smallville'); + expect(json.getText()).toContain('04501'); + expect(json.getText()).toContain('789 Elm'); + let nameOutput = element(by.css('hero-detail-7 > p ~ p')); + expect(nameOutput.getText()).toContain('Name value: Bombastic'); + let streetOutput = element(by.css('hero-detail-7 > p ~ p ~ p')); + expect(streetOutput.getText()).toContain('Street value: 789 Elm'); + }); + + it('should show three radio buttons', function() { + let radioButtons = element.all(by.css('input[formcontrolname=power]')); + expect(radioButtons.get(0).getAttribute('value')).toBe('flight'); + expect(radioButtons.get(1).getAttribute('value')).toBe('x-ray vision'); + expect(radioButtons.get(2).getAttribute('value')).toBe('strength'); + }); + + it('should show a checkbox', function() { + let checkbox = element(by.css('input[formcontrolname=sidekick]')); + expect(checkbox.getAttribute('checked')).toBe(null); + }); + + }); // Bombastic setValue form + + describe('Magneta setValue form', function() { + beforeEach(function() { + let magnetaButton = element.all(by.css('nav a')).get(2); + magnetaButton.click(); + }); + + it('should show a hero information when the button is clicked', function() { + let editMessage = element(by.css('.demo > div > div > h3')); + expect(editMessage.getText()).toBe('Editing: Magneta'); + }); + + it('should show a form with the selected hero information', function() { + let nameInput = element(by.css('input[formcontrolname=name]')); + expect(nameInput.getAttribute('value')).toBe('Magneta'); + }); + + it('should show three radio buttons', function() { + let radioButtons = element.all(by.css('input[formcontrolname=power]')); + expect(radioButtons.get(0).getAttribute('value')).toBe('flight'); + expect(radioButtons.get(1).getAttribute('value')).toBe('x-ray vision'); + expect(radioButtons.get(2).getAttribute('value')).toBe('strength'); + }); + + it('should show a checkbox', function() { + let checkbox = element(by.css('input[formcontrolname=sidekick]')); + expect(checkbox.getAttribute('checked')).toBe(null); + }); + + it('shows a json output from the form', function() { + let json = element(by.css('hero-detail-7 > p')); + expect(json.getText()).toContain('Magneta'); + let nameOutput = element(by.css('hero-detail-7 > p ~ p')); + expect(nameOutput.getText()).toContain('Name value: Magneta'); + let streetOutput = element(by.css('hero-detail-7 > p ~ p ~ p')); + expect(streetOutput.getText()).toContain('Street value:'); + }); + + }); // Magneta setValue form + }); // SetValue demo + +// *************Begin patchValue Demo test******************************* + + describe('patchValue demo', function() { + beforeEach(function() { + let SetValueOption = element.all(by.css('select option')).get(5); + SetValueOption.click(); + }); + + it('should show patchValue Demo', function() { + select.getAttribute('value').then(function(demo: string) { + expect(demo).toBe('PatchValue Demo'); + }); + }); + + it('does not select any hero by default', function() { + let heroSection = element(by.css('.demo > div > div')); + expect(heroSection.isPresent()).toBe(false); + }); + + it('refreshes the page upon button click', function() { + // We move to another page... + let whirlwindButton = element.all(by.css('nav a')).get(0); + whirlwindButton.click(); + let refresh = element(by.css('button')); + refresh.click(); + let heroSection = element(by.css('.demo > div > div')); + expect(heroSection.isPresent()).toBe(false); + }); + + describe('Whirlwind patchValue form', function() { + beforeEach(function() { + let whirlwindButton = element.all(by.css('nav a')).get(0); + whirlwindButton.click(); + }); + + it('should show a hero information when the button is clicked', function() { + let editMessage = element(by.css('h2 ~ h3')); + expect(editMessage.getText()).toBe('Editing: Whirlwind'); + }); + + it('should show a form with the selected hero information', function() { + let nameInput = element(by.css('input[formcontrolname=name]')); + expect(nameInput.getAttribute('value')).toBe('Whirlwind'); + let address1 = finalDemoAddressForm(element, 0); + expect(address1.street).toBe(''); + expect(address1.state).toBe(''); + expect(address1.zip).toBe(''); + expect(address1.city).toBe(''); + }); + + it('should show three radio buttons', function() { + let radioButtons = element.all(by.css('input[formcontrolname=power]')); + expect(radioButtons.get(0).getAttribute('value')).toBe('flight'); + expect(radioButtons.get(1).getAttribute('value')).toBe('x-ray vision'); + expect(radioButtons.get(2).getAttribute('value')).toBe('strength'); + }); + + it('should show a checkbox', function() { + let checkbox = element(by.css('input[formcontrolname=sidekick]')); + expect(checkbox.getAttribute('checked')).toBe(null); + }); + + it('shows a json output from the form', function() { + let json = element(by.css('hero-detail-6 > p')); + expect(json.getText()).toContain('Whirlwind'); + let nameOutput = element(by.css('hero-detail-6 > p ~ p')); + expect(nameOutput.getText()).toContain('Name value: Whirlwind'); + let streetOutput = element(by.css('hero-detail-6 > p ~ p ~ p')); + expect(streetOutput.getText()).toContain('Street value:'); + }); + + + }); // Bombastic patchValue form + describe('Bombastic patchValue form', function() { + beforeEach(function() { + let bombasticButton = element.all(by.css('nav a')).get(1); + bombasticButton.click(); + }); + + it('should show a hero information when the button is clicked', function() { + let editMessage = element(by.css('h2 ~ h3')); + expect(editMessage.getText()).toBe('Editing: Bombastic'); + }); + + it('should show a form with the selected hero information', function() { + let nameInput = element(by.css('input[formcontrolname=name]')); + expect(nameInput.getAttribute('value')).toBe('Bombastic'); + let address1 = finalDemoAddressForm(element, 0); + expect(address1.street).toBe(''); + expect(address1.state).toBe(''); + expect(address1.zip).toBe(''); + expect(address1.city).toBe(''); + }); + + it('should show three radio buttons', function() { + let radioButtons = element.all(by.css('input[formcontrolname=power]')); + expect(radioButtons.get(0).getAttribute('value')).toBe('flight'); + expect(radioButtons.get(1).getAttribute('value')).toBe('x-ray vision'); + expect(radioButtons.get(2).getAttribute('value')).toBe('strength'); + }); + + it('should show a checkbox', function() { + let checkbox = element(by.css('input[formcontrolname=sidekick]')); + expect(checkbox.getAttribute('checked')).toBe(null); + }); + + it('shows a json output from the form', function() { + let json = element(by.css('hero-detail-6 > p')); + expect(json.getText()).toContain('Bombastic'); + let nameOutput = element(by.css('hero-detail-6 > p ~ p')); + expect(nameOutput.getText()).toContain('Name value: Bombastic'); + let streetOutput = element(by.css('hero-detail-6 > p ~ p ~ p')); + expect(streetOutput.getText()).toContain('Street value:'); + }); + }); // Bombastic patchValue form + + describe('Magneta patchValue form', function() { + beforeEach(function() { + let magnetaButton = element.all(by.css('nav a')).get(2); + magnetaButton.click(); + }); + + it('should show a hero information when the button is clicked', function() { + let editMessage = element(by.css('h2 ~ h3')); + expect(editMessage.getText()).toBe('Editing: Magneta'); + }); + + it('should show a form with the selected hero information', function() { + let nameInput = element(by.css('input[formcontrolname=name]')); + expect(nameInput.getAttribute('value')).toBe('Magneta'); + let address1 = finalDemoAddressForm(element, 0); + expect(address1.street).toBe(''); + expect(address1.state).toBe(''); + expect(address1.zip).toBe(''); + expect(address1.city).toBe(''); + }); + + it('should show three radio buttons', function() { + let radioButtons = element.all(by.css('input[formcontrolname=power]')); + expect(radioButtons.get(0).getAttribute('value')).toBe('flight'); + expect(radioButtons.get(1).getAttribute('value')).toBe('x-ray vision'); + expect(radioButtons.get(2).getAttribute('value')).toBe('strength'); + }); + + it('should show a checkbox', function() { + let checkbox = element(by.css('input[formcontrolname=sidekick]')); + expect(checkbox.getAttribute('checked')).toBe(null); + }); + + it('shows a json output from the form', function() { + let json = element(by.css('hero-detail-6 > p')); + expect(json.getText()).toContain('Magneta'); + let nameOutput = element(by.css('hero-detail-6 > p ~ p')); + expect(nameOutput.getText()).toContain('Name value: Magneta'); + let streetOutput = element(by.css('hero-detail-6 > p ~ p ~ p')); + expect(streetOutput.getText()).toContain('Street value:'); + }); + + }); // Magneta patchValue form + }); // PatchValue demo + + + +// *************Begin Nested FormBuilder Demo test******************************* + + describe('Nested FormBuilder demo', function() { + beforeEach(function() { + let NestedFormBuilderOption = element.all(by.css('select option')).get(4); + NestedFormBuilderOption.click(); + }); + + it('should show Nested FormBuilder Demo', function() { + select.getAttribute('value').then(function(demo: string) { + expect(demo).toBe('Nested FormBuilder group Demo'); + }); + }); + + it('should show a form for hero information', function() { + let nameInput = element(by.css('input[formcontrolname=name]')); + expect(nameInput.getAttribute('value')).toBe(''); + let address1 = finalDemoAddressForm(element, 0); + expect(address1.street).toBe(''); + expect(address1.state).toBe(''); + expect(address1.zip).toBe(''); + expect(address1.city).toBe(''); + }); + + it('should show three radio buttons', function() { + let radioButtons = element.all(by.css('input[formcontrolname=power]')); + expect(radioButtons.get(0).getAttribute('value')).toBe('flight'); + expect(radioButtons.get(1).getAttribute('value')).toBe('x-ray vision'); + expect(radioButtons.get(2).getAttribute('value')).toBe('strength'); + }); + + it('should show a checkbox', function() { + let checkbox = element(by.css('input[formcontrolname=sidekick]')); + expect(checkbox.getAttribute('checked')).toBe(null); + }); + + it('shows a json output from the form', function() { + let json = element(by.css('hero-detail-5 > p')); + expect(json.getText()).toContain('address'); + let nameOutput = element(by.css('hero-detail-5 > p ~ p')); + expect(nameOutput.getText()).toContain('Name value:'); + let streetOutput = element(by.css('hero-detail-5 > p ~ p ~ p')); + expect(streetOutput.getText()).toContain('Street value:'); + }); + + }); // Nested FormBuilder demo + +// *************Begin Group with multiple controls Demo test******************************* + + describe('Group with multiple controls demo', function() { + beforeEach(function() { + let NestedFormBuilderOption = element.all(by.css('select option')).get(3); + NestedFormBuilderOption.click(); + }); + + it('should show Group with multiple controls Demo', function() { + select.getAttribute('value').then(function(demo: string) { + expect(demo).toBe('Group with multiple controls Demo'); + }); + }); + + it('should show header', function() { + let header = element(by.css('hero-detail-4 > h3')); + expect(header.getText()).toBe('A FormGroup with multiple FormControls'); + }); + + it('should show a form for hero information', function() { + let nameInput = element(by.css('input[formcontrolname=name]')); + expect(nameInput.getAttribute('value')).toBe(''); + let address1 = finalDemoAddressForm(element, 0); + expect(address1.street).toBe(''); + expect(address1.state).toBe(''); + expect(address1.zip).toBe(''); + expect(address1.city).toBe(''); + }); + + it('should show three radio buttons', function() { + let radioButtons = element.all(by.css('input[formcontrolname=power]')); + expect(radioButtons.get(0).getAttribute('value')).toBe('flight'); + expect(radioButtons.get(1).getAttribute('value')).toBe('x-ray vision'); + expect(radioButtons.get(2).getAttribute('value')).toBe('strength'); + }); + it('should show a checkbox', function() { + let checkbox = element(by.css('input[formcontrolname=sidekick]')); + expect(checkbox.getAttribute('checked')).toBe(null); + }); + it('shows a json output from the form', function() { + let json = element(by.css('hero-detail-4 > p')); + expect(json.getText()).toContain('power'); + }); + +}); // Group with multiple controls demo + + + +// *************Begin Group with multiple controls Demo test******************************* + + describe('Simple FormBuilder Group demo', function() { + beforeEach(function() { + let SimpleFormBuilderOption = element.all(by.css('select option')).get(2); + SimpleFormBuilderOption.click(); + }); + + it('should show Simple FormBuilder group Demo', function() { + select.getAttribute('value').then(function(demo: string) { + expect(demo).toBe('Simple FormBuilder group Demo'); + }); + }); + + it('should show header', function() { + let header = element(by.css('hero-detail-3 > h3')); + expect(header.getText()).toBe('A FormGroup with a single FormControl using FormBuilder'); + }); + + it('should show a form for hero information', function() { + let nameInput = element(by.css('input[formcontrolname=name]')); + expect(nameInput.getAttribute('value')).toBe(''); + }); + + it('shows a json output from the form', function() { + let json = element(by.css('hero-detail-3 > p')); + expect(json.getText()).toContain('name'); + let validStatus = element(by.css('hero-detail-3 > p ~ p')); + expect(validStatus.getText()).toContain('INVALID'); + }); + +}); // Group with multiple controls demo + + +// *************Begin FormControl in a FormGroup Demo test******************************* + + describe('FormControl in a FormGroup demo', function() { + beforeEach(function() { + let SimpleFormBuilderOption = element.all(by.css('select option')).get(1); + SimpleFormBuilderOption.click(); + }); + + it('should show FormControl in a FormGroup Demo', function() { + select.getAttribute('value').then(function(demo: string) { + expect(demo).toBe('FormControl in a FormGroup Demo'); + }); + }); + + it('should show header', function() { + let header = element(by.css('hero-detail-2 > h3')); + expect(header.getText()).toBe('FormControl in a FormGroup'); + }); + + it('should show a form for hero information', function() { + let nameInput = element(by.css('input[formcontrolname=name]')); + expect(nameInput.getAttribute('value')).toBe(''); + }); + + it('shows a json output from the form', function() { + let json = element(by.css('hero-detail-2 > p')); + expect(json.getText()).toContain('name'); + }); + +}); // Group with multiple controls demo + +// *************Begin Just A FormControl Demo test******************************* + + describe('Just a FormControl demo', function() { + beforeEach(function() { + let FormControlOption = element.all(by.css('select option')).get(0); + FormControlOption.click(); + }); + + it('should show Just a FormControl demo', function() { + select.getAttribute('value').then(function(demo: string) { + expect(demo).toBe('Just a FormControl Demo'); + }); + }); + + it('should show header', function() { + let header = element(by.css('hero-detail-1 > h3')); + expect(header.getText()).toBe('Just a FormControl'); + }); + + it('should show a form for hero information', function() { + let nameInput = element(by.css('input')); + expect(nameInput.getAttribute('value')).toBe(''); + }); + + }); // Just a FormControl demo test + + +}); // reactive forms diff --git a/public/docs/_examples/reactive-forms/ts/app/app.component.1.ts b/public/docs/_examples/reactive-forms/ts/app/app.component.1.ts new file mode 100644 index 0000000000..4ab3949863 --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/app.component.1.ts @@ -0,0 +1,13 @@ +// #docregion +import { Component } from '@angular/core'; + +@Component({ + moduleId: module.id, + selector: 'my-app', + template: ` +
+

Reactive Forms

+ +
` +}) +export class AppComponent { } diff --git a/public/docs/_examples/reactive-forms/ts/app/app.component.ts b/public/docs/_examples/reactive-forms/ts/app/app.component.ts new file mode 100644 index 0000000000..e41b9f8b35 --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/app.component.ts @@ -0,0 +1,13 @@ +// #docregion +import { Component } from '@angular/core'; + +@Component({ + moduleId: module.id, + selector: 'my-app', + template: ` +
+

Reactive Forms

+ +
` +}) +export class AppComponent { } diff --git a/public/docs/_examples/reactive-forms/ts/app/app.module.ts b/public/docs/_examples/reactive-forms/ts/app/app.module.ts new file mode 100644 index 0000000000..b9f2ea8f99 --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/app.module.ts @@ -0,0 +1,39 @@ +// #docplaster +// #docregion +// #docregion v1 +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { ReactiveFormsModule } from '@angular/forms'; // <-- #1 import module + +import { AppComponent } from './app.component'; +import { HeroDetailComponent } from './hero-detail.component'; // <-- #1 import component +// #enddocregion v1 +import { HeroListComponent } from './hero-list.component'; + +import { HeroService } from './hero.service'; // <-- #1 import service +// #docregion v1 + +@NgModule({ + imports: [ + BrowserModule, + ReactiveFormsModule // <-- #2 add to Angular module imports + ], + declarations: [ + AppComponent, + HeroDetailComponent, // <-- #3 declare app component +// #enddocregion v1 + HeroListComponent +// #docregion v1 + ], +// #enddocregion v1 + exports: [ // export for the DemoModule + AppComponent, + HeroDetailComponent, + HeroListComponent + ], + providers: [ HeroService ], // <-- #4 provide HeroService +// #docregion v1 + bootstrap: [ AppComponent ] +}) +export class AppModule { } +// #enddocregion v1 diff --git a/public/docs/_examples/reactive-forms/ts/app/data-model.ts b/public/docs/_examples/reactive-forms/ts/app/data-model.ts new file mode 100644 index 0000000000..aaebfacb47 --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/data-model.ts @@ -0,0 +1,40 @@ +// #docregion +// #docregion model-classes +export class Hero { + id = 0; + name = ''; + addresses: Address[]; +} + +export class Address { + street = ''; + city = ''; + state = ''; + zip = ''; +} +// #enddocregion model-classes + +export const heroes: Hero[] = [ + { + id: 1, + name: 'Whirlwind', + addresses: [ + {street: '123 Main', city: 'Anywhere', state: 'CA', zip: '94801'}, + {street: '456 Maple', city: 'Somewhere', state: 'VA', zip: '23226'}, + ] + }, + { + id: 2, + name: 'Bombastic', + addresses: [ + {street: '789 Elm', city: 'Smallville', state: 'OH', zip: '04501'}, + ] + }, + { + id: 3, + name: 'Magneta', + addresses: [ ] + }, +]; + +export const states = ['CA', 'MD', 'OH', 'VA']; diff --git a/public/docs/_examples/reactive-forms/ts/app/demo.component.html b/public/docs/_examples/reactive-forms/ts/app/demo.component.html new file mode 100644 index 0000000000..be74a7bc53 --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/demo.component.html @@ -0,0 +1,40 @@ +
+

Reactive Forms

+

Pick a demo: + +

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

Loading heroes ...

+

Select a hero:

+ + + +
+
+

Hero Detail

+

Editing: {{selectedHero.name}}

+ + + + +
+
+
+
diff --git a/public/docs/_examples/reactive-forms/ts/app/demo.component.ts b/public/docs/_examples/reactive-forms/ts/app/demo.component.ts new file mode 100644 index 0000000000..d24d7b5a38 --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/demo.component.ts @@ -0,0 +1,48 @@ +/* tslint:disable:member-ordering */ +import { Component } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; + +import { Hero } from './data-model'; +import { HeroService } from './hero.service'; + +@Component({ + moduleId: module.id, + selector: 'my-app', + templateUrl: 'demo.component.html' +}) +export class DemoComponent { + + demos: string[] = [ + 'Just a FormControl', + 'FormControl in a FormGroup', + 'Simple FormBuilder group', + 'Group with multiple controls', + 'Nested FormBuilder group', + 'PatchValue', + 'SetValue', + 'FormArray', + 'Final'].map(n => n + ' Demo'); + + final = this.demos.length; + demo = this.final; // current demo + + heroes: Observable; + isLoading = false; + selectedHero: Hero; + + constructor(private heroService: HeroService) { } + + getHeroes() { + this.isLoading = true; + this.heroes = this.heroService.getHeroes() + .finally(() => this.isLoading = false); + this.selectedHero = undefined; + } + + select(hero: Hero) { this.selectedHero = hero; } + + selectDemo(demo: number) { + this.demo = demo + 1; + this.getHeroes(); + } +} diff --git a/public/docs/_examples/reactive-forms/ts/app/demo.module.ts b/public/docs/_examples/reactive-forms/ts/app/demo.module.ts new file mode 100644 index 0000000000..4dd5d29d25 --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/demo.module.ts @@ -0,0 +1,33 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { ReactiveFormsModule } from '@angular/forms'; + +import { AppModule } from './app.module'; +import { DemoComponent } from './demo.component'; +import { HeroDetailComponent1 } from './hero-detail-1.component'; +import { HeroDetailComponent2 } from './hero-detail-2.component'; +import { HeroDetailComponent3 } from './hero-detail-3.component'; +import { HeroDetailComponent4 } from './hero-detail-4.component'; +import { HeroDetailComponent5 } from './hero-detail-5.component'; +import { HeroDetailComponent6 } from './hero-detail-6.component'; +import { HeroDetailComponent7 } from './hero-detail-7.component'; +import { HeroDetailComponent8 } from './hero-detail-8.component'; + +@NgModule({ + imports: [ + BrowserModule, + ReactiveFormsModule, + AppModule, + ], + declarations: [ DemoComponent, + HeroDetailComponent1, + HeroDetailComponent2, + HeroDetailComponent3, + HeroDetailComponent4, + HeroDetailComponent5, + HeroDetailComponent6, + HeroDetailComponent7, + HeroDetailComponent8], + bootstrap: [ DemoComponent ] +}) +export class DemoModule { } diff --git a/public/docs/_examples/reactive-forms/ts/app/hero-detail-1.component.html b/public/docs/_examples/reactive-forms/ts/app/hero-detail-1.component.html new file mode 100644 index 0000000000..7217708d22 --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/hero-detail-1.component.html @@ -0,0 +1,8 @@ + +

Hero Detail

+

Just a FormControl

+ + + diff --git a/public/docs/_examples/reactive-forms/ts/app/hero-detail-1.component.ts b/public/docs/_examples/reactive-forms/ts/app/hero-detail-1.component.ts new file mode 100644 index 0000000000..586ca36e76 --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/hero-detail-1.component.ts @@ -0,0 +1,15 @@ +/* tslint:disable:component-class-suffix */ +// #docregion imports +import { Component } from '@angular/core'; +import { FormControl } from '@angular/forms'; +// #enddocregion + +@Component({ + moduleId: module.id, + selector: 'hero-detail-1', + templateUrl: './hero-detail-1.component.html' +}) +// #docregion v1 +export class HeroDetailComponent1 { + name = new FormControl(); +} diff --git a/public/docs/_examples/reactive-forms/ts/app/hero-detail-2.component.html b/public/docs/_examples/reactive-forms/ts/app/hero-detail-2.component.html new file mode 100644 index 0000000000..1e98354842 --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/hero-detail-2.component.html @@ -0,0 +1,18 @@ + +

Hero Detail

+

FormControl in a FormGroup

+
+
+ +
+
+ + + +

Form value: {{ heroForm.value | json }}

+ + + + diff --git a/public/docs/_examples/reactive-forms/ts/app/hero-detail-2.component.ts b/public/docs/_examples/reactive-forms/ts/app/hero-detail-2.component.ts new file mode 100644 index 0000000000..e3c0448a7f --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/hero-detail-2.component.ts @@ -0,0 +1,18 @@ +/* tslint:disable:component-class-suffix */ +// #docregion imports +import { Component } from '@angular/core'; +import { FormControl, FormGroup } from '@angular/forms'; +// #enddocregion imports + +@Component({ + moduleId: module.id, + selector: 'hero-detail-2', + templateUrl: './hero-detail-2.component.html' +}) +// #docregion v2 +export class HeroDetailComponent2 { + heroForm = new FormGroup ({ + name: new FormControl() + }); +} +// #enddocregion v2 diff --git a/public/docs/_examples/reactive-forms/ts/app/hero-detail-3.component.html b/public/docs/_examples/reactive-forms/ts/app/hero-detail-3.component.html new file mode 100644 index 0000000000..8edc544dd4 --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/hero-detail-3.component.html @@ -0,0 +1,16 @@ + +

Hero Detail

+

A FormGroup with a single FormControl using FormBuilder

+
+
+ +
+
+ + + +

Form value: {{ heroForm.value | json }}

+

Form status: {{ heroForm.status | json }}

+ diff --git a/public/docs/_examples/reactive-forms/ts/app/hero-detail-3.component.ts b/public/docs/_examples/reactive-forms/ts/app/hero-detail-3.component.ts new file mode 100644 index 0000000000..400c6911d6 --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/hero-detail-3.component.ts @@ -0,0 +1,28 @@ +/* tslint:disable:component-class-suffix */ +// #docregion imports +import { Component } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +// #enddocregion imports + +@Component({ + moduleId: module.id, + selector: 'hero-detail-3', + templateUrl: './hero-detail-3.component.html' +}) +// #docregion v3 +export class HeroDetailComponent3 { + heroForm: FormGroup; // <--- heroForm is of type FormGroup + + constructor(private fb: FormBuilder) { // <--- inject FormBuilder + this.createForm(); + } + + createForm() { + // #docregion required + this.heroForm = this.fb.group({ + name: ['', Validators.required ], + }); + // #enddocregion required + } +} +// #enddocregion v3 diff --git a/public/docs/_examples/reactive-forms/ts/app/hero-detail-3a.component.ts b/public/docs/_examples/reactive-forms/ts/app/hero-detail-3a.component.ts new file mode 100644 index 0000000000..b76803d7ed --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/hero-detail-3a.component.ts @@ -0,0 +1,26 @@ +/* tslint:disable:component-class-suffix */ +// #docregion imports +import { Component } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +// #enddocregion imports + +@Component({ + moduleId: module.id, + selector: 'hero-detail-3', + templateUrl: './hero-detail-3.component.html' +}) +// #docregion v3a +export class HeroDetailComponent3 { + heroForm: FormGroup; // <--- heroForm is of type FormGroup + + constructor(private fb: FormBuilder) { // <--- inject FormBuilder + this.createForm(); + } + + createForm() { + this.heroForm = this.fb.group({ + name: '', // <--- the FormControl called "name" + }); + } +} +// #enddocregion v3a diff --git a/public/docs/_examples/reactive-forms/ts/app/hero-detail-4.component.html b/public/docs/_examples/reactive-forms/ts/app/hero-detail-4.component.html new file mode 100644 index 0000000000..de8bb49e24 --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/hero-detail-4.component.html @@ -0,0 +1,46 @@ + +

Hero Detail

+

A FormGroup with multiple FormControls

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

Form value: {{ heroForm.value | json }}

diff --git a/public/docs/_examples/reactive-forms/ts/app/hero-detail-4.component.ts b/public/docs/_examples/reactive-forms/ts/app/hero-detail-4.component.ts new file mode 100644 index 0000000000..8705765a56 --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/hero-detail-4.component.ts @@ -0,0 +1,35 @@ +/* tslint:disable:component-class-suffix */ +// #docregion imports +import { Component } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +import { states } from './data-model'; +// #enddocregion imports + +@Component({ + moduleId: module.id, + selector: 'hero-detail-4', + templateUrl: './hero-detail-4.component.html' +}) +// #docregion v4 +export class HeroDetailComponent4 { + heroForm: FormGroup; + states = states; + + constructor(private fb: FormBuilder) { + this.createForm(); + } + + createForm() { + this.heroForm = this.fb.group({ + name: ['', Validators.required ], + street: '', + city: '', + state: '', + zip: '', + power: '', + sidekick: '' + }); + } +} +// #enddocregion v4 diff --git a/public/docs/_examples/reactive-forms/ts/app/hero-detail-5.component.html b/public/docs/_examples/reactive-forms/ts/app/hero-detail-5.component.html new file mode 100644 index 0000000000..078a263ae8 --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/hero-detail-5.component.html @@ -0,0 +1,56 @@ + +
+
+ +
+ +
+

Secret Lair

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

heroForm value: {{ heroForm.value | json}}

+

Extra info for the curious:

+ +

Name value: {{ heroForm.get('name').value }}

+ + + +

Street value: {{ heroForm.get('address.street').value}}

+ diff --git a/public/docs/_examples/reactive-forms/ts/app/hero-detail-5.component.ts b/public/docs/_examples/reactive-forms/ts/app/hero-detail-5.component.ts new file mode 100644 index 0000000000..2bafd0709e --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/hero-detail-5.component.ts @@ -0,0 +1,36 @@ +/* tslint:disable:component-class-suffix */ +import { Component } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +import { states } from './data-model'; + +@Component({ + moduleId: module.id, + selector: 'hero-detail-5', + templateUrl: './hero-detail-5.component.html' +}) +// #docregion v5 +export class HeroDetailComponent5 { + heroForm: FormGroup; + states = states; + + constructor(private fb: FormBuilder) { + this.createForm(); + } + + createForm() { + this.heroForm = this.fb.group({ // <-- the parent FormGroup + name: ['', Validators.required ], + address: this.fb.group({ // <-- the child FormGroup + street: '', + city: '', + state: '', + zip: '' + }), + power: '', + sidekick: '' + }); + } +} +// #enddocregion v5 + diff --git a/public/docs/_examples/reactive-forms/ts/app/hero-detail-6.component.html b/public/docs/_examples/reactive-forms/ts/app/hero-detail-6.component.html new file mode 100644 index 0000000000..c075731f12 --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/hero-detail-6.component.html @@ -0,0 +1,46 @@ + +

Hero Detail

+

PatchValue to initialize a value

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

Form value: {{ heroForm.value | json }}

diff --git a/public/docs/_examples/reactive-forms/ts/app/hero-detail-6.component.ts b/public/docs/_examples/reactive-forms/ts/app/hero-detail-6.component.ts new file mode 100644 index 0000000000..9c592a953e --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/hero-detail-6.component.ts @@ -0,0 +1,59 @@ +/* tslint:disable:component-class-suffix */ +// #docregion import-input +import { Component, Input, OnChanges } from '@angular/core'; +// #enddocregion import-input +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +// #docregion import-hero +import { Hero, states } from './data-model'; +// #enddocregion import-hero + +////////// 6 //////////////////// + +@Component({ + moduleId: module.id, + selector: 'hero-detail-6', + templateUrl: './hero-detail-5.component.html' +}) +// #docregion v6 +export class HeroDetailComponent6 implements OnChanges { + // #docregion hero + @Input() hero: Hero; + // #enddocregion hero + + heroForm: FormGroup; + states = states; + + constructor(private fb: FormBuilder) { + this.createForm(); + } + + createForm() { + // #docregion hero-form-model + this.heroForm = this.fb.group({ + name: ['', Validators.required ], + address: this.fb.group({ + street: '', + city: '', + state: '', + zip: '' + }), + power: '', + sidekick: '' + }); + // #enddocregion hero-form-model + } + + // #docregion patch-value-on-changes + ngOnChanges() { // <-- wrap patchValue in ngOnChanges + this.heroForm.reset(); + // #docregion patch-value + this.heroForm.patchValue({ + name: this.hero.name + }); + // #enddocregion patch-value + } + // #enddocregion patch-value-on-changes +} + +// #enddocregion v6 diff --git a/public/docs/_examples/reactive-forms/ts/app/hero-detail-7.component.html b/public/docs/_examples/reactive-forms/ts/app/hero-detail-7.component.html new file mode 100644 index 0000000000..ff24702b5a --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/hero-detail-7.component.html @@ -0,0 +1,46 @@ + +

Hero Detail

+

A FormGroup with multiple FormControls

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

Form value: {{ heroForm.value | json }}

diff --git a/public/docs/_examples/reactive-forms/ts/app/hero-detail-7.component.ts b/public/docs/_examples/reactive-forms/ts/app/hero-detail-7.component.ts new file mode 100644 index 0000000000..60a220b6bd --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/hero-detail-7.component.ts @@ -0,0 +1,67 @@ +/* tslint:disable:component-class-suffix */ +// #docplaster +// #docregion imports +import { Component, Input, OnChanges } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +import { Address, Hero, states } from './data-model'; +// #enddocregion imports + +@Component({ + moduleId: module.id, + selector: 'hero-detail-7', + templateUrl: './hero-detail-5.component.html' +}) +// #docregion v7 +export class HeroDetailComponent7 implements OnChanges { + @Input() hero: Hero; + + heroForm: FormGroup; + states = states; + + constructor(private fb: FormBuilder) { + this.createForm(); + } + + createForm() { + // #docregion address-form-group + this.heroForm = this.fb.group({ + name: ['', Validators.required ], + address: this.fb.group(new Address()), // <-- a FormGroup with a new address + power: '', + sidekick: '' + }); + // #enddocregion address-form-group + } + + // #docregion ngOnChanges + ngOnChanges() { + this.heroForm.reset({ + name: this.hero.name, + address: this.hero.addresses[0] || new Address() + }); + } + // #enddocregion ngOnChanges + + /* First version of ngOnChanges + // #docregion ngOnChanges-1 + ngOnChanges() + // #enddocregion ngOnChanges-1 + */ + ngOnChanges1() { + // #docregion reset + this.heroForm.reset(); + // #enddocregion reset + // #docregion ngOnChanges-1 + // #docregion set-value + this.heroForm.setValue({ + name: this.hero.name, + // #docregion set-value-address + address: this.hero.addresses[0] || new Address() + // #enddocregion set-value-address + }); + // #enddocregion set-value + } + // #enddocregion ngOnChanges-1 +} + diff --git a/public/docs/_examples/reactive-forms/ts/app/hero-detail-8.component.html b/public/docs/_examples/reactive-forms/ts/app/hero-detail-8.component.html new file mode 100644 index 0000000000..9993dd80ce --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/hero-detail-8.component.html @@ -0,0 +1,70 @@ + +

Using FormArray to add groups

+ +
+

Form Changed: {{ heroForm.dirty }}

+ +
+ +
+ + +
+
+ + +

Address #{{i + 1}}

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

heroForm value: {{ heroForm.value | json}}

diff --git a/public/docs/_examples/reactive-forms/ts/app/hero-detail-8.component.ts b/public/docs/_examples/reactive-forms/ts/app/hero-detail-8.component.ts new file mode 100644 index 0000000000..99cd6f822a --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/hero-detail-8.component.ts @@ -0,0 +1,69 @@ +/* tslint:disable:component-class-suffix */ +// #docregion imports +import { Component, Input, OnChanges } from '@angular/core'; +import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; + +import { Address, Hero, states } from './data-model'; +// #enddocregion imports + +@Component({ + moduleId: module.id, + selector: 'hero-detail-8', + templateUrl: './hero-detail-8.component.html' +}) +// #docregion v8 +export class HeroDetailComponent8 implements OnChanges { + @Input() hero: Hero; + + heroForm: FormGroup; + states = states; + + // #docregion ctor + constructor(private fb: FormBuilder) { + this.createForm(); + this.logNameChange(); + } + // #enddocregion ctor + + createForm() { + // #docregion secretLairs-form-array + this.heroForm = this.fb.group({ + name: ['', Validators.required ], + secretLairs: this.fb.array([]), // <-- secretLairs as an empty FormArray + power: '', + sidekick: '' + }); + // #enddocregion secretLairs-form-array + } + + logNameChange() {/* Coming soon */} + + // #docregion onchanges + ngOnChanges() { + this.heroForm.reset({ + name: this.hero.name + }); + this.setAddresses(this.hero.addresses); + } + // #enddocregion onchanges + + // #docregion get-secret-lairs + get secretLairs(): FormArray { + return this.heroForm.get('secretLairs') as FormArray; + }; + // #enddocregion get-secret-lairs + + // #docregion set-addresses + setAddresses(addresses: Address[]) { + const addressFGs = addresses.map(address => this.fb.group(address)); + const addressFormArray = this.fb.array(addressFGs); + this.heroForm.setControl('secretLairs', addressFormArray); + } + // #enddocregion set-addresses + + // #docregion add-lair + addLair() { + this.secretLairs.push(this.fb.group(new Address())); + } + // #enddocregion add-lair +} diff --git a/public/docs/_examples/reactive-forms/ts/app/hero-detail.component.html b/public/docs/_examples/reactive-forms/ts/app/hero-detail.component.html new file mode 100644 index 0000000000..974867706f --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/hero-detail.component.html @@ -0,0 +1,73 @@ + + + +
+
+   + +
+ + + +
+ +
+ +
+
+ +

Address #{{i + 1}}

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

heroForm value: {{ heroForm.value | json}}

+ + +

Name change log

+
{{name}}
+ diff --git a/public/docs/_examples/reactive-forms/ts/app/hero-detail.component.ts b/public/docs/_examples/reactive-forms/ts/app/hero-detail.component.ts new file mode 100644 index 0000000000..a644e54ffb --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/hero-detail.component.ts @@ -0,0 +1,108 @@ +// #docplaster +// #docregion +import { Component, Input, OnChanges } from '@angular/core'; +import { FormArray, FormBuilder, FormGroup } from '@angular/forms'; + +import { Address, Hero, states } from './data-model'; +// #docregion import-service +import { HeroService } from './hero.service'; +// #enddocregion import-service + +// #docregion metadata +@Component({ + moduleId: module.id, + selector: 'hero-detail', + templateUrl: './hero-detail.component.html' +}) +// #enddocregion metadata +export class HeroDetailComponent implements OnChanges { + @Input() hero: Hero; + + heroForm: FormGroup; + // #docregion log-name-change + nameChangeLog: string[] = []; + // #enddocregion log-name-change + states = states; + + // #docregion ctor + constructor( + private fb: FormBuilder, + private heroService: HeroService) { + + this.createForm(); + this.logNameChange(); + } + // #enddocregion ctor + + createForm() { + this.heroForm = this.fb.group({ + name: '', + secretLairs: this.fb.array([]), + power: '', + sidekick: '' + }); + } + + ngOnChanges() { + this.heroForm.reset({ + name: this.hero.name + }); + this.setAddresses(this.hero.addresses); + } + + get secretLairs(): FormArray { + return this.heroForm.get('secretLairs') as FormArray; + }; + + setAddresses(addresses: Address[]) { + const addressFGs = addresses.map(address => this.fb.group(address)); + const addressFormArray = this.fb.array(addressFGs); + this.heroForm.setControl('secretLairs', addressFormArray); + } + + addLair() { + this.secretLairs.push(this.fb.group(new Address())); + } + + // #docregion on-submit + onSubmit() { + this.hero = this.prepareSaveHero(); + this.heroService.updateHero(this.hero).subscribe(/* error handling */); + this.ngOnChanges(); + } + // #enddocregion on-submit + + // #docregion prepare-save-hero + prepareSaveHero(): Hero { + const formModel = this.heroForm.value; + + // deep copy of form model lairs + const secretLairsDeepCopy: Address[] = formModel.secretLairs.map( + (address: Address) => Object.assign({}, address) + ); + + // return new `Hero` object containing a combination of original hero value(s) + // and deep copies of changed form model values + const saveHero: Hero = { + id: this.hero.id, + name: formModel.name as string, + // addresses: formModel.secretLairs // <-- bad! + addresses: secretLairsDeepCopy + }; + return saveHero; + } + // #enddocregion prepare-save-hero + + // #docregion revert + revert() { this.ngOnChanges(); } + // #enddocregion revert + + // #docregion log-name-change + logNameChange() { + const nameControl = this.heroForm.get('name'); + nameControl.valueChanges.forEach( + (value: string) => this.nameChangeLog.push(value) + ); + } + // #enddocregion log-name-change +} diff --git a/public/docs/_examples/reactive-forms/ts/app/hero-list.component.1.html b/public/docs/_examples/reactive-forms/ts/app/hero-list.component.1.html new file mode 100644 index 0000000000..3bdc700564 --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/hero-list.component.1.html @@ -0,0 +1,8 @@ + + + +
+ +
diff --git a/public/docs/_examples/reactive-forms/ts/app/hero-list.component.html b/public/docs/_examples/reactive-forms/ts/app/hero-list.component.html new file mode 100644 index 0000000000..9767b7a318 --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/hero-list.component.html @@ -0,0 +1,17 @@ + +

Loading heroes ...

+

Select a hero:

+ + + +
+
+

Hero Detail

+

Editing: {{selectedHero.name}}

+ + + +
diff --git a/public/docs/_examples/reactive-forms/ts/app/hero-list.component.ts b/public/docs/_examples/reactive-forms/ts/app/hero-list.component.ts new file mode 100644 index 0000000000..b91c6413ce --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/hero-list.component.ts @@ -0,0 +1,32 @@ +// #docregion +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/operator/finally'; + +import { Hero } from './data-model'; +import { HeroService } from './hero.service'; + +@Component({ + moduleId: module.id, + selector: 'hero-list', + templateUrl: 'hero-list.component.html' +}) +export class HeroListComponent implements OnInit { + heroes: Observable; + isLoading = false; + selectedHero: Hero; + + constructor(private heroService: HeroService) { } + + ngOnInit() { this.getHeroes(); } + + getHeroes() { + this.isLoading = true; + this.heroes = this.heroService.getHeroes() + // Todo: error handling + .finally(() => this.isLoading = false); + this.selectedHero = undefined; + } + + select(hero: Hero) { this.selectedHero = hero; } +} diff --git a/public/docs/_examples/reactive-forms/ts/app/hero.service.ts b/public/docs/_examples/reactive-forms/ts/app/hero.service.ts new file mode 100644 index 0000000000..4a815e6dbe --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/hero.service.ts @@ -0,0 +1,26 @@ +// #docregion +import { Injectable } from '@angular/core'; + +import { Observable } from 'rxjs/Observable'; +import { of } from 'rxjs/observable/of'; +import 'rxjs/add/operator/delay'; + +import { Hero, heroes } from './data-model'; + +@Injectable() +export class HeroService { + + delayMs = 500; + + // Fake server get; assume nothing can go wrong + getHeroes(): Observable { + return of(heroes).delay(this.delayMs); // simulate latency with delay + } + + // Fake server update; assume nothing can go wrong + updateHero(hero: Hero): Observable { + const oldHero = heroes.find(h => h.id === hero.id); + const newHero = Object.assign(oldHero, hero); // Demo: mutate cached hero + return of(newHero).delay(this.delayMs); // simulate latency with delay + } +} diff --git a/public/docs/_examples/reactive-forms/ts/app/main-final.ts b/public/docs/_examples/reactive-forms/ts/app/main-final.ts new file mode 100644 index 0000000000..0f69d20ded --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/main-final.ts @@ -0,0 +1,5 @@ +// tslint:disable:no-unused-variable +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { AppModule } from './app.module'; + +platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/public/docs/_examples/reactive-forms/ts/app/main.ts b/public/docs/_examples/reactive-forms/ts/app/main.ts new file mode 100644 index 0000000000..c81ec2c80c --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/app/main.ts @@ -0,0 +1,6 @@ +// tslint:disable:no-unused-variable +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { AppModule } from './app.module'; // just the final version +import { DemoModule } from './demo.module'; // demo picker + +platformBrowserDynamic().bootstrapModule(DemoModule); // (AppModule); diff --git a/public/docs/_examples/reactive-forms/ts/example-config.json b/public/docs/_examples/reactive-forms/ts/example-config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/public/docs/_examples/reactive-forms/ts/final.plnkr.json b/public/docs/_examples/reactive-forms/ts/final.plnkr.json new file mode 100644 index 0000000000..3d3de08c39 --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/final.plnkr.json @@ -0,0 +1,20 @@ +{ + "description": "Angular Reactive Forms (final)", + "files":[ + "styles.css", + + "app/app.component.ts", + "app/app.module.ts", + "app/data-model.ts", + "app/hero.service.ts", + "app/hero-detail.component.html", + "app/hero-detail.component.ts", + "app/hero-list.component.html", + "app/hero-list.component.ts", + + "app/main-final.ts", + "index-final.html" + ], + "main": "index-final.html", + "tags": ["reactive", "forms"] +} diff --git a/public/docs/_examples/reactive-forms/ts/index-final.html b/public/docs/_examples/reactive-forms/ts/index-final.html new file mode 100644 index 0000000000..556891d495 --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/index-final.html @@ -0,0 +1,31 @@ + + + + + Hero Form + + + + + + + + + + + + + + + + + + + + + Loading... + + + diff --git a/public/docs/_examples/reactive-forms/ts/index.html b/public/docs/_examples/reactive-forms/ts/index.html new file mode 100644 index 0000000000..38a65811fd --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/index.html @@ -0,0 +1,31 @@ + + + + + Hero Form + + + + + + + + + + + + + + + + + + + + + Loading... + + + diff --git a/public/docs/_examples/reactive-forms/ts/plnkr.json b/public/docs/_examples/reactive-forms/ts/plnkr.json new file mode 100644 index 0000000000..770a142c05 --- /dev/null +++ b/public/docs/_examples/reactive-forms/ts/plnkr.json @@ -0,0 +1,14 @@ +{ + "description": "Angular Reactive Forms (Demo runner)", + "files":[ + "!**/*.d.ts", + "!**/*.js", + + "!app/app.component.1.ts", + "!app/hero-list.component.1.html", + + "!app/main-final.ts", + "!index-final.html" + ], + "tags": ["reactive", "forms"] +} diff --git a/public/docs/ts/latest/guide/_data.json b/public/docs/ts/latest/guide/_data.json index be7525f021..30987f9add 100644 --- a/public/docs/ts/latest/guide/_data.json +++ b/public/docs/ts/latest/guide/_data.json @@ -138,6 +138,11 @@ "intro": "Angular's hierarchical dependency injection system supports nested injectors in parallel with the component tree." }, + "reactive-forms": { + "title": "Reactive Forms", + "intro": "Create a reactive form using FormBuilder, groups, and arrays." + }, + "server-communication": { "title": "HTTP Client", "intro": "Use an HTTP Client to talk to a remote server." diff --git a/public/docs/ts/latest/guide/change-log.jade b/public/docs/ts/latest/guide/change-log.jade index 7f4240bbcb..baaf95c268 100644 --- a/public/docs/ts/latest/guide/change-log.jade +++ b/public/docs/ts/latest/guide/change-log.jade @@ -5,6 +5,14 @@ block includes The Angular documentation is a living document with continuous improvements. This log calls attention to recent significant changes. + ## NEW: Reactive Forms guide (2017-01-31) + The new [**Reactive Forms**](reactive-forms.html) guide explains how and why to build a "reactive form". + "Reactive Forms" are the code-based counterpart to the declarative "Template Driven" forms approach + introduced in the [Forms](forms.html) guide. + Check it out before you decide how to add forms to your app. + Remember also that you can use both techniques in the same app, + choosing the approach that best fits each scenario. + ## NEW: Deployment guide (2017-01-30) The new [Deployment](deployment.html) guide describes techniques for putting your application on a server. It includes important advice on optimizing for production. diff --git a/public/docs/ts/latest/guide/forms.jade b/public/docs/ts/latest/guide/forms.jade index 4d9c97f970..f60325d981 100644 --- a/public/docs/ts/latest/guide/forms.jade +++ b/public/docs/ts/latest/guide/forms.jade @@ -668,7 +668,7 @@ figure.image-display .file hero-form.component.html .file hero-form.component.ts .file main.ts - .file node_modules ... + .file node_modules ... .file index.html .file package.json .file tsconfig.json diff --git a/public/docs/ts/latest/guide/reactive-forms.jade b/public/docs/ts/latest/guide/reactive-forms.jade new file mode 100644 index 0000000000..c72d138e34 --- /dev/null +++ b/public/docs/ts/latest/guide/reactive-forms.jade @@ -0,0 +1,1045 @@ +include ../_util-fns + +:marked + _Reactive forms_ is an Angular technique for creating forms in a _reactive_ style. + This guide explains reactive forms as you follow the steps to build a "Hero Detail Editor" form. + +a#toc +:marked + ## Contents + + - [Introduction to reactive forms](#intro) + - [Setup](#setup) + - [Create a data model](#data-model) + - [Create a _reactive forms_ component](#create-component) + - [Create its template file](#create-template) + - [Import the _ReactiveFormsModule_](#import) + - [Display the _HeroDetailComponent_](#update) + - [Add a FormGroup](#formgroup) + - [Taking a look at the form model](#json) + - [Introduction to _FormBuilder_](#formbuilder) + - [Validators.required](#validators) + - [Nested FormGroups](#grouping) + - [Inspect _FormControl_ properties](#properties) + - [Set form model data using _setValue_ and _patchValue_](#set-data) + - [Use _FormArray_ to present an array of _FormGroups_](#form-array) + - [Observe control changes](#observe-control) + - [Save form data](#save) + + Try the Reactive Forms live-example. + + You can also run the Reactive Forms Demo version + and choose one of the intermediate steps from the "demo picker" at the top. + +a#intro +:marked + ## Introduction to Reactive Forms + + Angular offers two form-building technologies: _reactive_ forms and _template-driven_ forms. + The two technologies belong to the `@angular/forms` library + and share a common set of form control classes. + + But they diverge markedly in philosophy, programming style, and technique. + They even have their own modules: the `ReactiveFormsModule` and the `FormsModule`. + + ### _Reactive_ forms + Angular _reactive_ forms facilitate a _reactive style_ of programming + that favors explicit management of the data flowing between + a non-UI _data model_ (typically retrieved from a server) and a + UI-oriented _form model_ that retains the states + and values of the HTML controls on screen. Reactive forms offer the ease + of using reactive patterns, testing, and validation. + + With _reactive_ forms, you create a tree of Angular form control objects + in the component class and bind them to native form control elements in the + component template, using techniques described in this guide. + + You create and manipulate form control objects directly in the + component class. As the component class has immediate access to both the data + model and the form control structure, you can push data model values into + the form controls and pull user-changed values back out. The component can + observe changes in form control state and react to those changes. + + One advantage of working with form control objects directly is that value and validity updates + are [always synchronous and under your control](#async-vs-sync "Async vs sync"). + You won't encounter the timing issues that sometimes plague a template-driven form + and reactive forms can be easier to unit test. + + In keeping with the reactive paradigm, the component + preserves the immutability of the _data model_, + treating it as a pure source of original values. + Rather than update the data model directly, + the component extracts user changes and forwards them to an external component or service, + which does something with them (such as saving them) + and returns a new _data model_ to the component that reflects the updated model state. + + Using reactive form directives does not require you to follow all reactive priniciples, + but it does facilitate the reactive programming approach should you choose to use it. + + ### _Template-driven_ forms + + _Template-driven_ forms, introduced in the [Template guide](forms.html), take a completely different approach. + + You place HTML form controls (such as `` and ``. + + +.l-sub-section + :marked + Disregard the `form-control` _CSS_ class. It belongs to the + Bootstrap CSS library, + not Angular. + It _styles_ the form but in no way impacts the logic of the form. + +a#import +:marked + ## Import the _ReactiveFormsModule_ + + The HeroDetailComponent template uses `formControlName` + directive from the `ReactiveFormsModule`. + + In this sample, you declare the `HeroDetailComponent` in the `AppModule`. + Therefore, do the following three things in `app.module.ts`: + 1. Use a JavaScript `import` statement to access + the `ReactiveFormsModule` and the `HeroDetailComponent`. + 1. Add `ReactiveFormsModule` to the `AppModule`'s `imports` list. + 1. Add `HeroDetailComponent` to the declarations array. + ++makeExample('reactive-forms/ts/app/app.module.ts', 'v1','app/app.module.ts (excerpt)')(format=".") + +a#update +.l-main-section +:marked + ## Display the _HeroDetailComponent_ + Revise the `AppComponent` template so it displays the `HeroDetailComponent`. ++makeExample('reactive-forms/ts/app/app.component.1.ts', '','app/app.component.ts')(format=".") + +a#essentials +:marked + ### Essential form classes + It may be helpful to read a brief description of the core form classes. + + * [_AbstractControl_](../api/forms/index/AbstractControl-class.html-class.html "API Reference: AbstractControl") + is the abstract base class for the three concrete form control classes: + `FormControl`, `FormGroup`, and `FormArray`. + It provides their common behaviors and properties, some of which are _observable_. + + * [_FormControl_](../api/forms/index/FormControl-class.html "API Reference: FormControl") + tracks the value and validity status of an _individual_ form control. + It corresponds to an HTML form control such as an input box or selector. + + * [_FormGroup_](../api/forms/index/FormGroup-class.html "API Reference: FormGroup") + tracks the value and validity state of a _group_ of `AbstractControl` instances. + The group's properties include its child controls. + The top-level form in your component is a `FormGroup`. + + * [_FormArray_](../api/forms/index/FormArray-class.html "API Reference: FormArray") + tracks the value and validity state of a numerically indexed _array_ of `AbstractControl` instances. + + You'll learn more about these classes as you work through this guide. + + +:marked + ### Style the app + You used bootstrap CSS classes in the template HTML of both the `AppComponent` and the `HeroDetailComponent`. + Add the `bootstrap` _CSS stylesheet_ to the head of `index.html`: + ++makeExample('reactive-forms/ts/index.html', 'bootstrap','index.html')(format=".") + +:marked + Now that everything is wired up, the browser should display something like this: + +figure.image-display + img(src="/resources/images/devguide/reactive-forms/just-formcontrol.png" width="400px" alt="Single FormControl") + +a#formgroup +:marked + ## Add a FormGroup + Usually, if you have multiple *FormControls*, you'll want to register + them within a parent `FormGroup`. + This is simple to do. To add a `FormGroup`, add it to the imports section + of `hero-detail.component.ts`: + ++makeExample('reactive-forms/ts/app/hero-detail-2.component.ts', 'imports','app/hero-detail.component.ts')(format=".") + +:marked + In the class, wrap the `FormControl` in a `FormGroup` called `heroForm` as follows: + ++makeExample('reactive-forms/ts/app/hero-detail-2.component.ts', 'v2','app/hero-detail.component.ts')(format=".") + +:marked + Now that you've made changes in the class, they need to be reflected in the + template. Update `hero-detail.component.html` by replacing it with the following. + ++makeExample('reactive-forms/ts/app/hero-detail-2.component.html', 'basic-form','app/hero-detail.component.html')(format=".") + +:marked + Notice that now the single input is in a `form` element. The `novalidate` + attribute in the `
` element prevents the browser + from attempting native HTML validations. + + `formGroup` is a reactive form directive that takes an existing + `FormGroup` instance and associates it with an HTML element. + In this case, it will associate the `FormGroup` you saved as + `heroForm` with the form element. + +.l-sub-section + :marked + Disregard the `form-group` _CSS_ class. It belongs to the + Bootstrap CSS library, + not Angular. + Like the `form-control` class, it _styles_ the form + but in no way impacts its logic. + + +:marked + The form looks great. But does it work? + When the user enters a name, where does the value go? + +a#json +:marked + ## Taking a look at the form model + + The value goes into the **_form model_** that backs the group's `FormControls`. + To see the form model, add the following line after the + closing `form` tag in the `hero-detail.component.html`: + ++makeExample('reactive-forms/ts/app/hero-detail-3.component.html', 'form-value-json','app/hero-detail.component.html')(format=".") + +:marked + The `heroForm.value` returns the _form model_. + Piping it through the `JsonPipe` renders the model as JSON in the browser: + +figure.image-display + img(src="/resources/images/devguide/reactive-forms/json-output.png" width="400px" alt="JSON output") + +:marked + The initial `name` property value is the empty string. + Type into the _name_ input box and watch the keystokes appear in the JSON. + + + +:marked + Great! You have the basics of a form. + + In real life apps, forms get big fast. + `FormBuilder` makes form development and maintenance easier. + + +.l-main-section +a#formbuilder +:marked + ## Introduction to _FormBuilder_ + + The `FormBuilder` class helps reduce repetition and + clutter by handling details of control creation for you. + + To use `FormBuilder`, you need to import it into `hero-detail.component.ts`: ++makeExample('reactive-forms/ts/app/hero-detail-3a.component.ts', 'imports','app/hero-detail.component.ts (excerpt)')(format=".") + +:marked + Use it now to refactor the `HeroDetailComponent` into something that's a little easier to read and write, + by following this plan: + + * Explicitly declare the type of the `heroForm` property to be `FormGroup`; you'll initialize it later. + * Inject a `FormBuilder` into the constructor. + * Add a new method that uses the `FormBuilder` to define the `heroForm`; call it `createForm`. + * Call `createForm` in the constructor. + + The revised `HeroDetailComponent` looks like this: ++makeExample('reactive-forms/ts/app/hero-detail-3a.component.ts', 'v3a','app/hero-detail.component.ts (excerpt)')(format=".") + +:marked + `FormBuilder.group` is a factory method that creates a `FormGroup`.   + `FormBuilder.group` takes an object whose keys and values are `FormControl` names and their definitions. + In this example, the `name` control is defined by its initial data value, an empty string. + + Defining a group of controls in a single object makes for a compact, readable style. + It beats writing an equivalent series of `new FormControl(...)` statements. + +a#validators +:marked + ### Validators.required + Though this guide doesn't go deeply into validations, here is one example that + demonstrates the simplicity of using `Validators.required` in reactive forms. + + First, import the `Validators` symbol. ++makeExample('reactive-forms/ts/app/hero-detail-3.component.ts', 'imports','app/hero-detail.component.ts (excerpt)')(format=".") + +:marked + To make the `name` `FormControl` required, replace the `name` + property in the `FormGroup` with an array. + The first item is the initial value for `name`; + the second is the required validator, `Validators.required`. + ++makeExample('reactive-forms/ts/app/hero-detail-3.component.ts', 'required','app/hero-detail.component.ts (excerpt)')(format=".") +.l-sub-section + :marked + Reactive validators are simple, composable functions. + Configuring validation is harder in template-driven forms where you must wrap validators in a directive. +:marked + Update the diagnostic message at the bottom of the template to display the form's validity status. + ++makeExample('reactive-forms/ts/app/hero-detail-3.component.html', 'form-value-json','app/hero-detail.component.html (excerpt)')(format=".") + +:marked + The browser displays the following: + +figure.image-display + img(src="/resources/images/devguide/reactive-forms/validators-json-output.png" width="400px" alt="Single FormControl") + +:marked + `Validators.required` is working. The status is `INVALID` because the input box has no value. + Type into the input box to see the status change from `INVALID` to `VALID`. + + In a real app, you'd replace the diagnosic message with a user-friendly experience. + +:marked + Using `Validators.required` is optional for the rest of the guide. + It remains in each of the following examples with the same configuration. + + For more on validating Angular forms, see the + [Form Validation](../cookbook/form-validation.html) guide. + +:marked + ### More FormControls + A hero has more than a name. + A hero has an address, a super power and sometimes a sidekick too. + + The address has a state property. The user will select a state with a `
\ No newline at end of file diff --git a/public/resources/live-examples/reactive-forms/ts/final-plnkr.html b/public/resources/live-examples/reactive-forms/ts/final-plnkr.html new file mode 100644 index 0000000000..a9fe278c0a --- /dev/null +++ b/public/resources/live-examples/reactive-forms/ts/final-plnkr.html @@ -0,0 +1,948 @@ +
\ No newline at end of file