diff --git a/aio/content/examples/reactive-forms/debug.log b/aio/content/examples/reactive-forms/debug.log deleted file mode 100644 index 9c0793f598..0000000000 --- a/aio/content/examples/reactive-forms/debug.log +++ /dev/null @@ -1,3 +0,0 @@ -[1030/162525.401:ERROR:process_reader_win.cc(123)] NtOpenThread: {Acceso denegado} Un proceso ha solicitado acceso a un objeto, pero no se le han concedido esos derechos de acceso. (0xc0000022) -[1030/162525.402:ERROR:exception_snapshot_win.cc(87)] thread ID 26896 not found in process -[1030/162525.402:WARNING:crash_report_exception_handler.cc(62)] ProcessSnapshotWin::Initialize failed diff --git a/aio/content/examples/reactive-forms/e2e/src/app.e2e-spec.ts b/aio/content/examples/reactive-forms/e2e/src/app.e2e-spec.ts index 59c1ea797d..25022d2290 100644 --- a/aio/content/examples/reactive-forms/e2e/src/app.e2e-spec.ts +++ b/aio/content/examples/reactive-forms/e2e/src/app.e2e-spec.ts @@ -2,1019 +2,144 @@ 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 () { + const nameEditor = element(by.css('app-name-editor')); + const profileEditor = element(by.css('app-profile-editor')); + const nameEditorLink = element(by.cssContainingText('app-root > nav > a', 'Name Editor')); + const profileEditorLink = element(by.cssContainingText('app-root > nav > a', 'Profile Editor')); -describe('Reactive forms', function() { - let select: any; - - beforeEach(function() { + beforeAll(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'); + describe('Name Editor', function () { + const nameInput = nameEditor.element(by.css('input')); + const updateButton = nameEditor.element(by.buttonText('Update Name')); + const nameText = 'John Smith'; + + beforeAll(async () => { + await nameEditorLink.click(); }); - it('should contain a dropdown with each example', function() { - expect(select.isDisplayed()).toBe(true); + beforeEach(async () => { + await nameInput.clear(); }); - it('should have 9 options for different demos', function() { - let options = select.all(by.tagName('option')); - expect(options.count()).toBe(9); + it('should update the name value when the name control is updated', async () => { + await nameInput.sendKeys(nameText); + + const value = await nameInput.getAttribute('value'); + + expect(value).toBe(nameText); }); - it('should start with Final Demo', function() { - select.getAttribute('value').then(function(demo: string) { - expect(demo).toBe('Final Demo'); - }); + it('should update the name control when the Update Name button is clicked', async () => { + await nameInput.sendKeys(nameText); + const value = await nameInput.getAttribute('value'); + + expect(value).toBe(nameText); + await updateButton.click(); + + const value = await nameInput.getAttribute('value'); + + expect(value).toBe('Nancy'); + }); + + it('should update the displayed control value when the name control updated', async () => { + await nameInput.sendKeys(nameText); + const valueElement = nameEditor.element(by.cssContainingText('p', 'Value:')); + const nameValueElement = await valueElement.getText(); + const nameValue = nameValueElement.toString().replace('Value: ', ''); + + expect(nameValue).toBe(nameText); }); }); -// *************Begin Final Demo test******************************* + describe('Profile Editor', function () { + const firstNameInput = getInput('firstName'); + const lastNameInput = getInput('lastName'); + const streetInput = getInput('street'); + const addAliasButton = element(by.buttonText('Add Alias')); + const updateButton = profileEditor.element(by.buttonText('Update Profile')); + const profile = { + firstName: 'John', + lastName: 'Smith', + street: '345 South Lane', + city: 'Northtown', + state: 'XX', + zip: 12345 + }; - describe('final demo', function() { - it('does not select any hero by default', function() { - let heroSection = element(by.css('app-hero-list > div')); - expect(heroSection.isPresent()).toBe(false); + beforeAll(async () => { + await profileEditorLink.click(); }); - 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('app-hero-list > div')); - expect(heroSection.isPresent()).toBe(false); + beforeEach(async () => { + await browser.get(''); + await profileEditorLink.click(); }); - 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('app-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('app-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('app-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('app-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('app-hero-detail > form > div > button')).get(0); - save.click(); - let editMessage = element(by.css('app-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('app-hero-detail > form > div > button')).get(1); - revert.click(); - let editMessage = element(by.css('app-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); - }); + it('should be invalid by default', async () => { + expect(await profileEditor.getText()).toContain('Form Status: INVALID'); }); - describe('Bombastic form', function() { - beforeEach(function() { - let bombastaButton = element.all(by.css('nav a')).get(1); - bombastaButton.click(); - }); + it('should be valid if the First Name is filled in', async () => { + await firstNameInput.clear(); + await firstNameInput.sendKeys('John Smith'); - it('should show a hero information when the button is clicked', function() { - let editMessage = element(by.css('app-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('app-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('app-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('app-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('app-hero-detail > form > div > button')).get(0); - save.click(); - let editMessage = element(by.css('app-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('app-hero-detail > form > div > button')).get(1); - revert.click(); - let editMessage = element(by.css('app-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); - }); + expect(await profileEditor.getText()).toContain('Form Status: VALID'); }); - describe('Magneta form', function() { + it('should update the name when the button is clicked', async () => { + await firstNameInput.clear(); + await streetInput.clear(); + await firstNameInput.sendKeys('John'); + await streetInput.sendKeys('345 Smith Lane'); + const firstNameInitial = await firstNameInput.getAttribute('value'); + const streetNameInitial = await streetInput.getAttribute('value'); - beforeEach(function() { - let magnetaButton = element.all(by.css('nav a')).get(2); - magnetaButton.click(); - }); + expect(firstNameInitial).toBe('John'); + expect(streetNameInitial).toBe('345 Smith Lane'); + await updateButton.click(); - it('should show hero information when the button is clicked', function() { - let editMessage = element(by.css('app-hero-list > div > h3')); - expect(editMessage.getText()).toBe('Editing: Magneta'); - }); + const nameValue = await firstNameInput.getAttribute('value'); + const streetValue = await streetInput.getAttribute('value'); - 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('app-hero-detail > p')); - expect(json.getText()).toContain('Magneta'); - }); - - it('has two disabled buttons by default', function() { - let buttons = element.all(by.css('app-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('app-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('app-hero-detail > form > div > button')).get(0); - save.click(); - let editMessage = element(by.css('app-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('app-hero-detail > form > div > button')).get(1); - revert.click(); - let editMessage = element(by.css('app-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(); + expect(nameValue).toBe('Nancy'); + expect(streetValue).toBe('123 Drew Street'); }); - it('should show FormArray Demo', function() { - select.getAttribute('value').then(function(demo: string) { - expect(demo).toBe('FormArray Demo'); - }); - }); + it('should add an alias field when the Add Alias button is clicked', async () => { + await addAliasButton.click(); - it('does not select any hero by default', function() { - let heroSection = element(by.css('app-hero-list > div')); - expect(heroSection.isPresent()).toBe(false); + const aliasInputs = profileEditor.all(by.cssContainingText('label', 'Alias')); + + expect(await aliasInputs.count()).toBe(2); }); - 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('app-hero-list > div')); - expect(heroSection.isPresent()).toBe(false); + it('should update the displayed form value when form inputs are updated', async () => { + const aliasText = 'Johnny'; + const inputs = await Promise.all( + Object.keys(profile) + .map(key => + getInput(key).sendKeys(`${profile[key]}`) + ) + ); + + const aliasInputs = profileEditor.all(by.cssContainingText('label', 'Alias')); + const aliasInput = aliasInputs.get(0).element(by.css('input')); + await aliasInput.sendKeys(aliasText); + const formValueElement = profileEditor.all(by.cssContainingText('p', 'Form Value:')); + const formValue = await formValueElement.getText(); + const formJson = JSON.parse(formValue.toString().replace('Form Value:', '')); + + expect(profile.firstName).toBe(formJson.firstName); + expect(profile.lastName).toBe(formJson.lastName); + expect(formJson.aliases[0]).toBe(aliasText); }); + }); - 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('app-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('app-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('app-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('app-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('app-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('app-hero-detail-7 > p')); - expect(json.getText()).toContain('Whirlwind'); - expect(json.getText()).toContain('Anywhere'); - let nameOutput = element(by.css('app-hero-detail-7 > p ~ p')); - expect(nameOutput.getText()).toContain('Name value: Whirlwind'); - let streetOutput = element(by.css('app-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('app-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('app-hero-detail-7 > p ~ p')); - expect(nameOutput.getText()).toContain('Name value: Bombastic'); - let streetOutput = element(by.css('app-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('app-hero-detail-7 > p')); - expect(json.getText()).toContain('Magneta'); - let nameOutput = element(by.css('app-hero-detail-7 > p ~ p')); - expect(nameOutput.getText()).toContain('Name value: Magneta'); - let streetOutput = element(by.css('app-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('app-hero-detail-6 > p')); - expect(json.getText()).toContain('Whirlwind'); - let nameOutput = element(by.css('app-hero-detail-6 > p ~ p')); - expect(nameOutput.getText()).toContain('Name value: Whirlwind'); - let streetOutput = element(by.css('app-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('app-hero-detail-6 > p')); - expect(json.getText()).toContain('Bombastic'); - let nameOutput = element(by.css('app-hero-detail-6 > p ~ p')); - expect(nameOutput.getText()).toContain('Name value: Bombastic'); - let streetOutput = element(by.css('app-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('app-hero-detail-6 > p')); - expect(json.getText()).toContain('Magneta'); - let nameOutput = element(by.css('app-hero-detail-6 > p ~ p')); - expect(nameOutput.getText()).toContain('Name value: Magneta'); - let streetOutput = element(by.css('app-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('app-hero-detail-5 > p')); - expect(json.getText()).toContain('address'); - let nameOutput = element(by.css('app-hero-detail-5 > p ~ p')); - expect(nameOutput.getText()).toContain('Name value:'); - let streetOutput = element(by.css('app-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('app-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('app-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('app-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('app-hero-detail-3 > p')); - expect(json.getText()).toContain('name'); - let validStatus = element(by.css('app-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('app-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('app-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('app-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 + function getInput(key: string) { + return element(by.css(`input[formcontrolname=${key}`)); + } +}); diff --git a/aio/content/examples/reactive-forms/final.stackblitz.json b/aio/content/examples/reactive-forms/final.stackblitz.json deleted file mode 100644 index f789236245..0000000000 --- a/aio/content/examples/reactive-forms/final.stackblitz.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "description": "Angular Reactive Forms (final)", - "files":[ - "src/styles.css", - - "src/app/app.component.ts", - "src/app/app.component.html", - "src/app/app.component.css", - "src/app/app.module.ts", - "src/app/data-model.ts", - "src/app/hero.service.ts", - "src/app/hero-detail/hero-detail.component.html", - "src/app/hero-detail/hero-detail.component.ts", - "src/app/hero-detail/hero-detail.component.css", - "src/app/hero-list/hero-list.component.html", - "src/app/hero-list/hero-list.component.ts", - "src/app/hero-list/hero-list.component.css", - - "src/main-final.ts", - "src/index-final.html" - ], - "main": "src/index-final.html", - "tags": ["reactive", "forms"] -} diff --git a/aio/content/examples/reactive-forms/src/app/app.component.1.html b/aio/content/examples/reactive-forms/src/app/app.component.1.html index 14316fbe62..57ca7e22fb 100644 --- a/aio/content/examples/reactive-forms/src/app/app.component.1.html +++ b/aio/content/examples/reactive-forms/src/app/app.component.1.html @@ -1,4 +1,10 @@ -
-

Reactive Forms

- -
+ +

Reactive Forms

+ + + + + + + + \ No newline at end of file diff --git a/aio/content/examples/reactive-forms/src/app/app.component.html b/aio/content/examples/reactive-forms/src/app/app.component.html index b7d0e6d580..a953480634 100644 --- a/aio/content/examples/reactive-forms/src/app/app.component.html +++ b/aio/content/examples/reactive-forms/src/app/app.component.html @@ -1,4 +1,17 @@ -
-

Reactive Forms

- -
+ + +

Reactive Forms

+ + + + + + + + + + + diff --git a/aio/content/examples/reactive-forms/src/app/app.component.spec.ts b/aio/content/examples/reactive-forms/src/app/app.component.spec.ts new file mode 100644 index 0000000000..fff9fa666d --- /dev/null +++ b/aio/content/examples/reactive-forms/src/app/app.component.spec.ts @@ -0,0 +1,27 @@ +import { TestBed, async } from '@angular/core/testing'; +import { AppComponent } from './app.component'; +describe('AppComponent', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + AppComponent + ], + }).compileComponents(); + })); + it('should create the app', async(() => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app).toBeTruthy(); + })); + it(`should have as title 'app'`, async(() => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app.title).toEqual('app'); + })); + it('should render title in a h1 tag', async(() => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.debugElement.nativeElement; + expect(compiled.querySelector('h1').textContent).toContain('Welcome to reactive-forms!'); + })); +}); diff --git a/aio/content/examples/reactive-forms/src/app/app.component.ts b/aio/content/examples/reactive-forms/src/app/app.component.ts index f7baece9b5..5c9993b6b6 100644 --- a/aio/content/examples/reactive-forms/src/app/app.component.ts +++ b/aio/content/examples/reactive-forms/src/app/app.component.ts @@ -1,9 +1,24 @@ -// #docregion import { Component } from '@angular/core'; +export type EditorType = 'name' | 'profile'; + @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) -export class AppComponent { } +export class AppComponent { + editor: EditorType = 'name'; + + get showNameEditor() { + return this.editor === 'name'; + } + + get showProfileEditor() { + return this.editor === 'profile'; + } + + toggleEditor(type: EditorType) { + this.editor = type; + } +} diff --git a/aio/content/examples/reactive-forms/src/app/app.module.ts b/aio/content/examples/reactive-forms/src/app/app.module.ts index 39b074c178..9ccc26e4af 100644 --- a/aio/content/examples/reactive-forms/src/app/app.module.ts +++ b/aio/content/examples/reactive-forms/src/app/app.module.ts @@ -1,45 +1,34 @@ // #docplaster -// #docregion -// #docregion v1 -import { NgModule } from '@angular/core'; -import { BrowserModule } from '@angular/platform-browser'; -import { ReactiveFormsModule } from '@angular/forms'; // <-- #1 import module +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +// #docregion imports +import { ReactiveFormsModule } from '@angular/forms'; -import { AppComponent } from './app.component'; -import { HeroDetailComponent } from './hero-detail/hero-detail.component'; -// #enddocregion v1 -// #docregion hero-service-list -// add JavaScript imports -import { HeroListComponent } from './hero-list/hero-list.component'; -import { HeroService } from './hero.service'; -// #docregion v1 +// #enddocregion imports +import { AppComponent } from './app.component'; +import { NameEditorComponent } from './name-editor/name-editor.component'; +import { ProfileEditorComponent } from './profile-editor/profile-editor.component'; +// #docregion imports @NgModule({ +// #enddocregion imports declarations: [ AppComponent, - HeroDetailComponent, -// #enddocregion v1 - HeroListComponent // <--declare HeroListComponent -// #docregion v1 + NameEditorComponent, + ProfileEditorComponent ], - // #enddocregion hero-service-list +// #docregion imports imports: [ +// #enddocregion imports BrowserModule, - ReactiveFormsModule // <-- #2 add to @NgModule imports +// #docregion imports + // other imports ... + ReactiveFormsModule ], - // #enddocregion v1 - // export for the DemoModule - // #docregion hero-service-list - // ... - exports: [ - AppComponent, - HeroDetailComponent, - HeroListComponent // <-- export HeroListComponent - ], - providers: [ HeroService ], // <-- provide HeroService -// #enddocregion hero-service-list -// #docregion v1 - bootstrap: [ AppComponent ] +// #enddocregion imports + providers: [], + bootstrap: [AppComponent] +// #docregion imports }) export class AppModule { } -// #enddocregion v1 +// #enddocregion imports diff --git a/aio/content/examples/reactive-forms/src/app/data-model.ts b/aio/content/examples/reactive-forms/src/app/data-model.ts deleted file mode 100644 index ad01ddee56..0000000000 --- a/aio/content/examples/reactive-forms/src/app/data-model.ts +++ /dev/null @@ -1,40 +0,0 @@ -// #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/aio/content/examples/reactive-forms/src/app/demo.component.html b/aio/content/examples/reactive-forms/src/app/demo.component.html deleted file mode 100644 index 369427a739..0000000000 --- a/aio/content/examples/reactive-forms/src/app/demo.component.html +++ /dev/null @@ -1,40 +0,0 @@ -
-

Reactive Forms

-

Pick a demo: - -

- -
- -
- - - - - - - -
- -

Loading heroes ...

-

Select a hero:

- - - -
-
-

Hero Detail

-

Editing: {{selectedHero.name}}

- - - - -
-
-
-
diff --git a/aio/content/examples/reactive-forms/src/app/demo.component.ts b/aio/content/examples/reactive-forms/src/app/demo.component.ts deleted file mode 100644 index 71658e7b6b..0000000000 --- a/aio/content/examples/reactive-forms/src/app/demo.component.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* tslint:disable:member-ordering */ -import { Component } from '@angular/core'; -import { Observable } from 'rxjs'; -import { finalize } from 'rxjs/operators'; - -import { Hero } from './data-model'; -import { HeroService } from './hero.service'; - -@Component({ - selector: 'app-root', - 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().pipe( - finalize(() => this.isLoading = false) - ); - this.selectedHero = undefined; - } - - select(hero: Hero) { this.selectedHero = hero; } - - selectDemo(demo: number) { - this.demo = demo + 1; - this.getHeroes(); - } -} diff --git a/aio/content/examples/reactive-forms/src/app/demo.module.ts b/aio/content/examples/reactive-forms/src/app/demo.module.ts deleted file mode 100644 index a8b71ab5b6..0000000000 --- a/aio/content/examples/reactive-forms/src/app/demo.module.ts +++ /dev/null @@ -1,33 +0,0 @@ -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/hero-detail-1.component'; -import { HeroDetailComponent2 } from './hero-detail/hero-detail-2.component'; -import { HeroDetailComponent3 } from './hero-detail/hero-detail-3.component'; -import { HeroDetailComponent4 } from './hero-detail/hero-detail-4.component'; -import { HeroDetailComponent5 } from './hero-detail/hero-detail-5.component'; -import { HeroDetailComponent6 } from './hero-detail/hero-detail-6.component'; -import { HeroDetailComponent7 } from './hero-detail/hero-detail-7.component'; -import { HeroDetailComponent8 } from './hero-detail/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/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-1.component.html b/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-1.component.html deleted file mode 100644 index 7217708d22..0000000000 --- a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-1.component.html +++ /dev/null @@ -1,8 +0,0 @@ - -

Hero Detail

-

Just a FormControl

- - - diff --git a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-1.component.ts b/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-1.component.ts deleted file mode 100644 index 7ce88f3522..0000000000 --- a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-1.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* tslint:disable:component-class-suffix */ - -import { Component } from '@angular/core'; -// #docregion import -import { FormControl } from '@angular/forms'; -// #enddocregion import - -@Component({ - selector: 'app-hero-detail-1', - templateUrl: './hero-detail-1.component.html' -}) -// #docregion v1 -export class HeroDetailComponent1 { - name = new FormControl(); -} diff --git a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-2.component.html b/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-2.component.html deleted file mode 100644 index 79410c4a6d..0000000000 --- a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-2.component.html +++ /dev/null @@ -1,18 +0,0 @@ - -

Hero Detail

-

FormControl in a FormGroup

-
-
- -
-
- - - -

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

- - - - diff --git a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-2.component.ts b/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-2.component.ts deleted file mode 100644 index 6508904ffa..0000000000 --- a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-2.component.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* tslint:disable:component-class-suffix */ -// #docregion imports -import { Component } from '@angular/core'; -import { FormControl, FormGroup } from '@angular/forms'; -// #enddocregion imports - -@Component({ - selector: 'app-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/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-3.component.html b/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-3.component.html deleted file mode 100644 index 89c5513566..0000000000 --- a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-3.component.html +++ /dev/null @@ -1,16 +0,0 @@ - -

Hero Detail

-

A FormGroup with a single FormControl using FormBuilder

-
-
- -
-
- - - -

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

-

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

- diff --git a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-3.component.ts b/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-3.component.ts deleted file mode 100644 index 81388c8faa..0000000000 --- a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-3.component.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* tslint:disable:component-class-suffix */ -// #docregion imports -import { Component } from '@angular/core'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -// #enddocregion imports - -@Component({ - selector: 'app-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/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-3a.component.ts b/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-3a.component.ts deleted file mode 100644 index 7cc0b1ca8f..0000000000 --- a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-3a.component.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* tslint:disable:component-class-suffix */ -// #docregion imports -import { Component } from '@angular/core'; -import { FormBuilder, FormGroup } from '@angular/forms'; -// #enddocregion imports - -@Component({ - selector: 'app-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/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-4.component.html b/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-4.component.html deleted file mode 100644 index 8702eeb540..0000000000 --- a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-4.component.html +++ /dev/null @@ -1,46 +0,0 @@ - -

Hero Detail

-

A FormGroup with multiple FormControls

-
-
- -
-
- -
-
- -
-
- -
-
- -
-
-

Super power:

- - - -
-
- -
-
- - -

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

diff --git a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-4.component.ts b/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-4.component.ts deleted file mode 100644 index b9a61aa1ea..0000000000 --- a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-4.component.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* 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({ - selector: 'app-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/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-5.component.html b/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-5.component.html deleted file mode 100644 index bc73ce006e..0000000000 --- a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-5.component.html +++ /dev/null @@ -1,56 +0,0 @@ - -
-
- -
- -
-

Secret Lair

-
- -
-
- -
-
- -
-
- -
-
- -
-

Super power:

- - - -
-
- -
-
- -

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/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-5.component.ts b/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-5.component.ts deleted file mode 100644 index 1193265c5b..0000000000 --- a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-5.component.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* tslint:disable:component-class-suffix */ -import { Component } from '@angular/core'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; - -import { states } from '../data-model'; - -@Component({ - selector: 'app-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/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-6.component.html b/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-6.component.html deleted file mode 100644 index a6f352a9b9..0000000000 --- a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-6.component.html +++ /dev/null @@ -1,46 +0,0 @@ - -

Hero Detail

-

PatchValue to initialize a value

-
-
- -
-
- -
-
- -
-
- -
-
- -
-
-

Super power:

- - - -
-
- -
-
- - -

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

diff --git a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-6.component.ts b/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-6.component.ts deleted file mode 100644 index 042b21aaf7..0000000000 --- a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-6.component.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* 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({ - selector: 'app-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() { // <-- call rebuildForm in ngOnChanges - this.rebuildForm(); - } - // #enddocregion patch-value-on-changes - - // #docregion patch-value-rebuildform - rebuildForm() { // <-- wrap patchValue in rebuildForm - this.heroForm.reset(); - // #docregion patch-value - this.heroForm.patchValue({ - name: this.hero.name - }); - // #enddocregion patch-value - } - // #enddocregion patch-value-rebuildform -} - - - -// #enddocregion v6 diff --git a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-7.component.html b/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-7.component.html deleted file mode 100644 index 57b00ca92c..0000000000 --- a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-7.component.html +++ /dev/null @@ -1,46 +0,0 @@ - -

Hero Detail

-

A FormGroup with multiple FormControls

-
-
- -
-
- -
-
- -
-
- -
-
- -
-
-

Super power:

- - - -
-
- -
-
- - -

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

diff --git a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-7.component.ts b/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-7.component.ts deleted file mode 100644 index b70b2e879d..0000000000 --- a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-7.component.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* tslint:disable:component-class-suffix */ -// #docplaster -// #docregion imports -import { Component, Input, OnChanges } from '@angular/core'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; - -// #docregion import-address -import { Address, Hero, states } from '../data-model'; -// #enddocregion import-address - -// #enddocregion imports - -@Component({ - selector: 'app-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.rebuildForm(); - } - // #enddocregion ngOnChanges - - // #docregion rebuildForm - rebuildForm() { - this.heroForm.reset({ - name: this.hero.name, - // #docregion set-value-address - address: this.hero.addresses[0] || new Address() - // #enddocregion set-value-address - }); - } - // #enddocregion rebuildForm - - /* First version of rebuildForm */ - rebuildForm1() { - // #docregion reset - this.heroForm.reset(); - // #enddocregion reset - // #docregion set-value - this.heroForm.setValue({ - name: this.hero.name, - address: this.hero.addresses[0] || new Address() - }); - // #enddocregion set-value - } -} diff --git a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-8.component.html b/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-8.component.html deleted file mode 100644 index bfb591551e..0000000000 --- a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-8.component.html +++ /dev/null @@ -1,72 +0,0 @@ - -

Using FormArray to add groups

- -
-

Form Changed: {{ heroForm.dirty }}

- -
- -
- - - -
- -
- - -

Address #{{i + 1}}

-
-
- -
-
- -
-
- -
-
- -
-
-
- - -
- - - - - - - -
- - -
-

Super power:

- - - -
-
- -
-
- -

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

diff --git a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-8.component.ts b/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-8.component.ts deleted file mode 100644 index f3e09df7fe..0000000000 --- a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail-8.component.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* 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({ - selector: 'app-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.rebuildForm(); - } - // #enddocregion onchanges - - // #docregion rebuildform - rebuildForm() { - this.heroForm.reset({ - name: this.hero.name - }); - this.setAddresses(this.hero.addresses); - } -// #enddocregion rebuildform - - // #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/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail.component.css b/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail.component.css deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail.component.html b/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail.component.html deleted file mode 100644 index c5e77ba43d..0000000000 --- a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail.component.html +++ /dev/null @@ -1,73 +0,0 @@ - - - -
-
-   - -
- - - -
- -
- -
-
- -

Address #{{i + 1}}

-
-
- -
-
- -
-
- -
-
- -
-
-
- -
- -
- -
-

Super power:

- - - -
-
- -
-
- - -

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

- - -

Name change log

-
{{name}}
- diff --git a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail.component.ts b/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail.component.ts deleted file mode 100644 index 13ef66b0f9..0000000000 --- a/aio/content/examples/reactive-forms/src/app/hero-detail/hero-detail.component.ts +++ /dev/null @@ -1,113 +0,0 @@ -// #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 - -@Component({ - selector: 'app-hero-detail', - templateUrl: './hero-detail.component.html', - styleUrls: ['./hero-detail.component.css'] -}) - -// #docregion onchanges-implementation -export class HeroDetailComponent implements OnChanges { -// #enddocregion onchanges-implementation - @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.rebuildForm(); - } - - rebuildForm() { - 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.rebuildForm(); - } - // #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.rebuildForm(); } - // #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/aio/content/examples/reactive-forms/src/app/hero-list/hero-list.component.1.html b/aio/content/examples/reactive-forms/src/app/hero-list/hero-list.component.1.html deleted file mode 100644 index 409c9b2a5c..0000000000 --- a/aio/content/examples/reactive-forms/src/app/hero-list/hero-list.component.1.html +++ /dev/null @@ -1,8 +0,0 @@ - - - -
- -
diff --git a/aio/content/examples/reactive-forms/src/app/hero-list/hero-list.component.css b/aio/content/examples/reactive-forms/src/app/hero-list/hero-list.component.css deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/aio/content/examples/reactive-forms/src/app/hero-list/hero-list.component.html b/aio/content/examples/reactive-forms/src/app/hero-list/hero-list.component.html deleted file mode 100644 index 78356caeb2..0000000000 --- a/aio/content/examples/reactive-forms/src/app/hero-list/hero-list.component.html +++ /dev/null @@ -1,17 +0,0 @@ - -

Loading heroes ...

-

Select a hero:

- - - -
-
-

Hero Detail

-

Editing: {{selectedHero.name}}

- - - -
diff --git a/aio/content/examples/reactive-forms/src/app/hero-list/hero-list.component.ts b/aio/content/examples/reactive-forms/src/app/hero-list/hero-list.component.ts deleted file mode 100644 index 1243a0a5c0..0000000000 --- a/aio/content/examples/reactive-forms/src/app/hero-list/hero-list.component.ts +++ /dev/null @@ -1,32 +0,0 @@ -// #docregion -import { Component, OnInit } from '@angular/core'; -import { Observable } from 'rxjs'; -import { finalize } from 'rxjs/operators'; - -import { Hero } from '../data-model'; -import { HeroService } from '../hero.service'; - -@Component({ - selector: 'app-hero-list', - templateUrl: './hero-list.component.html', - styleUrls: ['./hero-list.component.css'] -}) -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 - .pipe(finalize(() => this.isLoading = false)); - this.selectedHero = undefined; - } - - select(hero: Hero) { this.selectedHero = hero; } -} diff --git a/aio/content/examples/reactive-forms/src/app/hero.service.ts b/aio/content/examples/reactive-forms/src/app/hero.service.ts deleted file mode 100644 index ff1caf064a..0000000000 --- a/aio/content/examples/reactive-forms/src/app/hero.service.ts +++ /dev/null @@ -1,25 +0,0 @@ -// #docregion -import { Injectable } from '@angular/core'; - -import { Observable, of } from 'rxjs'; -import { delay } from 'rxjs/operators'; - -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).pipe(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).pipe(delay(this.delayMs)); // simulate latency with delay - } -} diff --git a/aio/content/examples/reactive-forms/src/app/name-editor/name-editor.component.css b/aio/content/examples/reactive-forms/src/app/name-editor/name-editor.component.css new file mode 100644 index 0000000000..c89ab4cc0b --- /dev/null +++ b/aio/content/examples/reactive-forms/src/app/name-editor/name-editor.component.css @@ -0,0 +1,19 @@ +:host { + display: flex; + flex-direction: column; + padding-top: 24px; +} + +label { + display: block; + width: 6em; + margin: .5em 0; + color: #607D8B; + font-weight: bold; +} + +input { + height: 2em; + font-size: 1em; + padding-left: .4em; +} \ No newline at end of file diff --git a/aio/content/examples/reactive-forms/src/app/name-editor/name-editor.component.html b/aio/content/examples/reactive-forms/src/app/name-editor/name-editor.component.html new file mode 100644 index 0000000000..95f1948ffa --- /dev/null +++ b/aio/content/examples/reactive-forms/src/app/name-editor/name-editor.component.html @@ -0,0 +1,21 @@ + + + + + + + +

+ Value: {{ name.value }} +

+ + + + +

+ +

+ diff --git a/aio/content/examples/reactive-forms/src/app/name-editor/name-editor.component.ts b/aio/content/examples/reactive-forms/src/app/name-editor/name-editor.component.ts new file mode 100644 index 0000000000..b549b560ea --- /dev/null +++ b/aio/content/examples/reactive-forms/src/app/name-editor/name-editor.component.ts @@ -0,0 +1,22 @@ +// #docplaster +// #docregion create-control +import { Component } from '@angular/core'; +import { FormControl } from '@angular/forms'; + +@Component({ + selector: 'app-name-editor', + templateUrl: './name-editor.component.html', + styleUrls: ['./name-editor.component.css'] +}) +export class NameEditorComponent { + name = new FormControl(''); +// #enddocregion create-control + +// #docregion update-value + updateName() { + this.name.setValue('Nancy'); + } +// #enddocregion update-value +// #docregion create-control +} +// #enddocregion create-control diff --git a/aio/content/examples/reactive-forms/src/app/profile-editor/profile-editor.component.1.html b/aio/content/examples/reactive-forms/src/app/profile-editor/profile-editor.component.1.html new file mode 100644 index 0000000000..c1fd2a10d2 --- /dev/null +++ b/aio/content/examples/reactive-forms/src/app/profile-editor/profile-editor.component.1.html @@ -0,0 +1,67 @@ + + +
+ + + + + + + +
+

Address

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

Aliases

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

+ Form Value: {{ profileForm.value | json }} +

+ + +

+ +

+ \ No newline at end of file diff --git a/aio/content/examples/reactive-forms/src/app/profile-editor/profile-editor.component.1.ts b/aio/content/examples/reactive-forms/src/app/profile-editor/profile-editor.component.1.ts new file mode 100644 index 0000000000..94d388847e --- /dev/null +++ b/aio/content/examples/reactive-forms/src/app/profile-editor/profile-editor.component.1.ts @@ -0,0 +1,40 @@ +// #docplaster +// #docregion formgroup, nested-formgroup +import { Component } from '@angular/core'; +// #docregion imports +import { FormGroup, FormControl } from '@angular/forms'; +// #enddocregion imports + +@Component({ + selector: 'app-profile-editor', + templateUrl: './profile-editor.component.html', + styleUrls: ['./profile-editor.component.css'] +}) +export class ProfileEditorComponent { +// #docregion formgroup-compare + profileForm = new FormGroup({ + firstName: new FormControl(''), + lastName: new FormControl(''), +// #enddocregion formgroup + address: new FormGroup({ + street: new FormControl(''), + city: new FormControl(''), + state: new FormControl(''), + zip: new FormControl('') + }) +// #docregion formgroup + }); +// #enddocregion formgroup, nested-formgroup, formgroup-compare +// #docregion patch-value + updateProfile() { + this.profileForm.patchValue({ + firstName: 'Nancy', + address: { + street: '123 Drew Street' + } + }); + } +// #enddocregion patch-value +// #docregion formgroup, nested-formgroup +} +// #enddocregion formgroup diff --git a/aio/content/examples/reactive-forms/src/app/profile-editor/profile-editor.component.2.ts b/aio/content/examples/reactive-forms/src/app/profile-editor/profile-editor.component.2.ts new file mode 100644 index 0000000000..d482d70b01 --- /dev/null +++ b/aio/content/examples/reactive-forms/src/app/profile-editor/profile-editor.component.2.ts @@ -0,0 +1,58 @@ +// #docplaster +// #docregion form-builder +import { Component } from '@angular/core'; +// #docregion form-builder-imports +import { FormBuilder } from '@angular/forms'; +// #enddocregion form-builder-imports, form-builder +// #docregion form-array-imports +import { FormArray } from '@angular/forms'; +// #docregion form-builder-imports, form-builder +// #enddocregion form-builder-imports, form-array-imports + +@Component({ + selector: 'app-profile-editor', + templateUrl: './profile-editor.component.html', + styleUrls: ['./profile-editor.component.css'] +}) +export class ProfileEditorComponent { +// #docregion formgroup-compare + profileForm = this.fb.group({ + firstName: [''], + lastName: [''], + address: this.fb.group({ + street: [''], + city: [''], + state: [''], + zip: [''] + }), +// #enddocregion form-builder, formgroup-compare + aliases: this.fb.array([ + this.fb.control('') + ]) +// #docregion form-builder, formgroup-compare + }); +// #enddocregion form-builder, formgroup-compare + get aliases() { + return this.profileForm.get('aliases') as FormArray; + } + +// #docregion inject-form-builder, form-builder + + constructor(private fb: FormBuilder) { } +// #enddocregion inject-form-builder, form-builder + + updateProfile() { + this.profileForm.patchValue({ + firstName: 'Nancy', + address: { + street: '123 Drew Street' + } + }); + } + + addAlias() { + this.aliases.push(this.fb.control('')); + } +// #docregion form-builder +} +// #enddocregion form-builder diff --git a/aio/content/examples/reactive-forms/src/app/profile-editor/profile-editor.component.css b/aio/content/examples/reactive-forms/src/app/profile-editor/profile-editor.component.css new file mode 100644 index 0000000000..6d29164b1a --- /dev/null +++ b/aio/content/examples/reactive-forms/src/app/profile-editor/profile-editor.component.css @@ -0,0 +1,39 @@ +/* ProfileEditorComponent's private CSS styles */ +:host { + display: flex; + flex-direction: column; + padding-top: 24px; +} + +label { + display: block; + width: 6em; + margin: .5em 0; + color: #607D8B; + font-weight: bold; +} + +input { + height: 2em; + font-size: 1em; + padding-left: .4em; +} + +button { + font-family: Arial; + background-color: #eee; + border: none; + padding: 5px 10px; + border-radius: 4px; + cursor: pointer; +} + +button:hover { + background-color: #cfd8dc; +} + +button:disabled { + background-color: #eee; + color: #ccc; + cursor: auto; +} diff --git a/aio/content/examples/reactive-forms/src/app/profile-editor/profile-editor.component.html b/aio/content/examples/reactive-forms/src/app/profile-editor/profile-editor.component.html new file mode 100644 index 0000000000..86178e1c3c --- /dev/null +++ b/aio/content/examples/reactive-forms/src/app/profile-editor/profile-editor.component.html @@ -0,0 +1,80 @@ + + +
+ + + + + +
+

Address

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

Aliases

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

+ Form Value: {{ profileForm.value | json }} +

+ + + + +

+ Form Status: {{ profileForm.status }} +

+ + + +

+ +

+ diff --git a/aio/content/examples/reactive-forms/src/app/profile-editor/profile-editor.component.ts b/aio/content/examples/reactive-forms/src/app/profile-editor/profile-editor.component.ts new file mode 100644 index 0000000000..813c97d5c0 --- /dev/null +++ b/aio/content/examples/reactive-forms/src/app/profile-editor/profile-editor.component.ts @@ -0,0 +1,73 @@ +// #docplaster +// #docregion form-builder +import { Component } from '@angular/core'; +// #docregion form-builder-imports +import { FormBuilder } from '@angular/forms'; +// #enddocregion form-builder-imports +// #docregion validator-imports +import { Validators } from '@angular/forms'; +// #enddocregion validator-imports +// #docregion form-array-imports +import { FormArray } from '@angular/forms'; +// #enddocregion form-array-imports + +@Component({ + selector: 'app-profile-editor', + templateUrl: './profile-editor.component.html', + styleUrls: ['./profile-editor.component.css'] +}) +export class ProfileEditorComponent { +// #docregion required-validator, aliases + profileForm = this.fb.group({ + firstName: ['', Validators.required], + lastName: [''], + address: this.fb.group({ + street: [''], + city: [''], + state: [''], + zip: [''] + }), +// #enddocregion form-builder, required-validator + aliases: this.fb.array([ + this.fb.control('') + ]) +// #docregion form-builder, required-validator + }); +// #enddocregion form-builder, required-validator, aliases +// #docregion aliases-getter + + get aliases() { + return this.profileForm.get('aliases') as FormArray; + } + +// #enddocregion aliases-getter +// #docregion inject-form-builder, form-builder + constructor(private fb: FormBuilder) { } + +// #enddocregion inject-form-builder + + updateProfile() { + this.profileForm.patchValue({ + firstName: 'Nancy', + address: { + street: '123 Drew Street' + } + }); + } +// #enddocregion form-builder +// #docregion add-alias + + addAlias() { + this.aliases.push(this.fb.control('')); + } +// #enddocregion add-alias +// #docregion on-submit + + onSubmit() { + // TODO: Use EventEmitter with form value + console.warn(this.profileForm.value); + } +// #enddocregion on-submit +// #docregion form-builder +} +// #enddocregion form-builder diff --git a/aio/content/examples/reactive-forms/src/index-final.html b/aio/content/examples/reactive-forms/src/index-final.html deleted file mode 100644 index 13416ffe40..0000000000 --- a/aio/content/examples/reactive-forms/src/index-final.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - Hero Form - - - - - - - - - - - diff --git a/aio/content/examples/reactive-forms/src/index.html b/aio/content/examples/reactive-forms/src/index.html index 7121ad3455..4a6b8fdac0 100644 --- a/aio/content/examples/reactive-forms/src/index.html +++ b/aio/content/examples/reactive-forms/src/index.html @@ -2,10 +2,9 @@ - Hero Form + Angular Reactive Forms - diff --git a/aio/content/examples/reactive-forms/src/main-final.ts b/aio/content/examples/reactive-forms/src/main-final.ts deleted file mode 100644 index 0a1ea31596..0000000000 --- a/aio/content/examples/reactive-forms/src/main-final.ts +++ /dev/null @@ -1,13 +0,0 @@ -// tslint:disable:no-unused-variable -import { enableProdMode } from '@angular/core'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; - -import { AppModule } from './app/app.module'; -import { environment } from './environments/environment'; - -if (environment.production) { - enableProdMode(); -} - -platformBrowserDynamic().bootstrapModule(AppModule); - diff --git a/aio/content/examples/reactive-forms/src/main.ts b/aio/content/examples/reactive-forms/src/main.ts index bd1865a45b..6c835d9116 100644 --- a/aio/content/examples/reactive-forms/src/main.ts +++ b/aio/content/examples/reactive-forms/src/main.ts @@ -2,12 +2,11 @@ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { AppModule } from './app/app.module'; // just the final version -import { DemoModule } from './app/demo.module'; // demo picker +import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } -platformBrowserDynamic().bootstrapModule(DemoModule); +platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/aio/content/examples/reactive-forms/src/styles.1.css b/aio/content/examples/reactive-forms/src/styles.1.css deleted file mode 100644 index 167a66be4f..0000000000 --- a/aio/content/examples/reactive-forms/src/styles.1.css +++ /dev/null @@ -1 +0,0 @@ -@import url('https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap.min.css'); diff --git a/aio/content/examples/reactive-forms/stackblitz.json b/aio/content/examples/reactive-forms/stackblitz.json index 2b325c037b..5696d23db7 100644 --- a/aio/content/examples/reactive-forms/stackblitz.json +++ b/aio/content/examples/reactive-forms/stackblitz.json @@ -3,6 +3,7 @@ "files":[ "!**/*.d.ts", "!**/*.js", + "!**/*.[0-9].*", "!src/app/app.component.1.ts", "!src/app/hero-list.component.1.html", diff --git a/aio/content/guide/reactive-forms.md b/aio/content/guide/reactive-forms.md index 34cb42ecca..8e7467fe07 100644 --- a/aio/content/guide/reactive-forms.md +++ b/aio/content/guide/reactive-forms.md @@ -1,261 +1,465 @@ -# Reactive Forms +# Reactive forms + +_Reactive forms_ provide a model-driven approach to handling form inputs whose values change over time. This guide shows you how to create and update a simple form control, progress to using multiple controls in a group, validate form values, and implement more advanced forms. -_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} -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. - +Try the Reactive Forms live-example. {@a intro} +## Introduction to reactive forms -## Introduction to Reactive Forms +Reactive forms use an explicit and immutable approach to managing the state of a form at a given point in time. Each change to the form state returns a new state, which maintains the integrity of the model between changes. Reactive forms are built around observable streams, where form inputs and values are provided as streams of input values, also while giving you synchronous access to the data. This approach allows your templates to take advantage of these streams of form state changes, rather than to be dependent to them. -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. +Reactive forms also allow for easier testing because you have an assurance that your data is consistent and predictable when requested. Consumers outside your templates have access to the same streams, where they can manipulate that data safely. -But they diverge markedly in philosophy, programming style, and technique. -They even have their own modules: the `ReactiveFormsModule` and the `FormsModule`. +Reactive forms differ from template-driven forms in distinct ways. Reactive forms provide more predictability with synchronous access to the data model, immutability with observable operators, and change tracking through observable streams. If you prefer direct access to modify data in your template, template-driven forms are less explicit because they rely on directives embedded in the template, along with mutable data to track changes asynchronously. See the [Appendix](#appendix) for detailed comparisons between the two paradigms. -### 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. +## Getting started -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. +This section describes the key steps to add a single form control. The example allows a user to enter their name into an input field, captures that input value, and displays the current value of the form control element. -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. +### Step 1 - Register the `ReactiveFormsModule` -One advantage of working with form control objects directly is that value and validity updates -are [always synchronous and under your control](guide/reactive-forms#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. +To use reactive forms, import `ReactiveFormsModule` from the `@angular/forms` package and add it to your NgModule's `imports` array. -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 +### Step 2 - Import and create a new form control -_Template-driven_ forms, introduced in the [Template guide](guide/forms), take a completely different approach. - -You place HTML form controls (such as `` and `` element for the hero name. - -A `FormControl` constructor accepts three, optional arguments: -the initial data value, an array of validators, and an array of async validators. - - -
- -This simple control doesn't have data or validators. -In real apps, most form controls have both. For in-depth information on -`Validators`, see the [Form Validation](guide/form-validation) guide. +*Note*: For a more detailed list of classes and directives provided by the `ReactiveFormsModule`, see the [Reactive Forms API](#reactive-forms-api) section.
-{@a create-template} +Using the template binding syntax, the form control is now registered to the `name` input element in the template. The form control and DOM element communicate with each other: the view reflects changes in the model, and the model reflects changes in the view. -## Create the template +#### Display the component -Now update the component's template with the following markup. +The `FormControl` assigned to `name` is displayed once the component is added to a template. - + -To let Angular know that this is the input that you want to -associate to the `name` `FormControl` in the class, -you need `[formControl]="name"` in the template on the ``. +
+ Name Editor +
-
+## Managing control values -Disregard the `form-control` CSS class. It belongs to the -Bootstrap CSS library, -not Angular, and styles the form but in no way impacts the logic. +Reactive forms give you access to the form control state and value at a point in time. You can manipulate +the current state and value through the component class or the component template. The following examples display the value of a `FormControl` and change it. + +{@a display-value} + +### Display the control’s value + +Every `FormControl` provides its current value as an observable through the `valueChanges` property. You can listen to changes in the form’s value in the template using the `AsyncPipe` or in the component class using the `subscribe()` method. The `value` property also gives you a snapshot of the current value. + +Display the current value using interpolation in the template as shown in the following example. + + + + + +The displayed value changes as you update the form control element. + +Reactive forms also provide access to more information about a given control through properties and methods provided with each instance. These properties and methods of the underlying [AbstractControl](api/forms/AbstractControl) are used to control form state and determine when to display messages when handling validation. For more information, see [Simple Form Validation](#simple-form-validation) later in this guide. + +Read about other `FormControl` properties and methods in the [Reactive Forms API](#reactive-forms-api) section. + +### Replace the form control value + +Reactive forms have methods to change a control's value programmatically, which gives you the flexibility to update the control’s value without user interaction. The `FormControl` provides a `setValue()` method which updates the value of the form control and validates the structure of the value provided against the control’s structure. For example, when retrieving form data from a backend API or service, use the `setValue()` method to update the control to its new value, replacing the old value entirely. + +The following example adds a method to the component class to update the value of the control to _Nancy_ using the `setValue()` method. + + + + + +Update the template with a button to simulate a name update. Any value entered in the form control element before clicking the `Update Name` button will be reflected as its current value. + + + + + +Because the form model is the source of truth for the control, when you click the button the value of the input is also changed within the component class, overriding its current value. + +
+ Name Editor Update +
+ +
+ +*Note*: In this example, you are only using a single control, but when using the `setValue()` method with a `FormGroup` or `FormArray` the value needs to match the structure of the group or array.
-{@a import} +## Grouping form controls -## Import the `ReactiveFormsModule` +Just as a `FormControl` instance gives you control over a single input field, a `FormGroup` tracks the form state of a group of `FormControl` instances (for example, a form). Each control in `FormGroup` is tracked by name when creating the `FormGroup`. The following example shows how to manage multiple `FormControl` instances in a single group. -The `HeroDetailComponent` template uses the `formControlName` -directive from the `ReactiveFormsModule`. +Generate a `ProfileEditor` component and import the `FormGroup` and `FormControl` classes from the `@angular/forms` package. -Do the following two things in `app.module.ts`: + -1. Use a JavaScript `import` statement to access -the `ReactiveFormsModule`. -1. Add `ReactiveFormsModule` to the `AppModule`'s `imports` list. - - + ng generate component ProfileEditor -{@a update} - -## Display the `HeroDetailComponent` -Revise the `AppComponent` template so it displays the `HeroDetailComponent`. - - + -{@a essentials} +### Step 1 - Create a `FormGroup` -## Essential form classes -This guide uses four fundamental classes to build a reactive form: +Create a property in the component class named `profileForm` and set the property to a new instance of a `FormGroup`. To initialize the `FormGroup`, provide the constructor with an object of controls with their respective names. +For the profile form, add two `FormControl` instances with the names `firstName` and `lastName`. + + + + + +The individual form controls are now collected within a group. The `FormGroup` provides its model value as an object reduced from the values of each control in the group. A `FormGroup` instance has the same properties (such as `value`, `untouched`) and methods (such as `setValue()`) as a `FormControl` instance. + +### Step 2 - Associate the `FormGroup` model and view + +The `FormGroup` also tracks the status and changes of each of its controls, so if one of the control’s status or value changes, the parent control also emits a new status or value change. The model for the group is maintained from its members. After you define the model, you must update the template to reflect the model in the view. + + + + + +Note that just as the `FormGroup` contains a group of controls, the _profileForm_ `FormGroup` is bound to the `form` element with the `FormGroup` directive, creating a communication layer between the model and the form containing the inputs. The `formControlName` input provided by the `FormControlName` directive binds each individual input to the form control defined in the `FormGroup`. The form controls communicate with their respective elements. The also communicate changes to the `FormGroup`, which provides the source of truth for the model value. + +### Save form data + +The `ProfileEditor` component takes input from the user, but in a real scenario you want to capture the form value for further processing outside the component. The `FormGroup` directive listens for the `submit` event emitted by the `form` element and emits an `ngSubmit` event that you can bind to a callback function. + +Add an `ngSubmit` event listener to the `form` tag with the `onSubmit()` callback method. + + + + + +The `onSubmit()` method in the `ProfileEditor` component captures the current value of the `profileForm`. To keep the form encapsulated, to provide the form value outside the component, use an `EventEmitter`. The following example uses `console.warn` to log to the browser console. + + + + + +The `submit` event is emitted by the `form` tag using the native DOM event. You trigger the event by clicking a button with `submit` type. This allows the user to use the enter key to trigger submission after filling out the form. + +Add a `button` to the bottom of the form to trigger the form submission. + + + + + +
+ +*Note:* The button in the snippet above also has a `disabled` binding attached to it to disable the button when the `profileForm` is invalid. You aren't performing any validation yet, so the button is always enabled. Simple form validation is covered later in the [Form Validation](#simple-form-validation) section. + +
+ +#### Display the component + +The `ProileEditor` component that contains the form is displayed when added to a component template. + + + + + +The `ProfileEditor` allows you to manage the `FormControl` instances for the `firstName` and `lastName` controls within the `FormGroup`. + +
+ Profile Editor +
+ +## Nesting form groups + +When building complex forms, managing the different areas of information is easier in smaller sections, and some groups of information naturally fall into the same group. Using a nested `FormGroup` allows you to break large forms groups into smaller, more manageable ones. + +### Step 1 - Create a nested group + +An address is a good example of information that can be grouped together. A `FormGroup` can accept both `FormControl` and `FormGroup` instances as children. This makes composing complex form models easier to maintain and logically group together. To create a nested group in the `profileForm`, add a nested `address` `FormGroup`. + + + + + +In this example, the `address group` combines the current `firstName` and `lastName` controls with the new `street`, `city`, `state` and `zip` controls. Even though the `address` `FormGroup` is a child of the overall `profileForm` `FormGroup`, the same rules still apply with value and status changes. Changes in status and value from the nested form group will propagate up to the parent form group, maintaining consistency with the overall model. + +### Step 2 - Group the nested form in the template + +After you update the model in the component class, update the template to connect the `FormGroup` instance and its input elements. + +Add the `address` form group containing the `firstName` and `lastName` fields to the `ProfileEditor` template. + + + + + +The `ProfileEditor` form is displayed as one group, but the model is broken down further to represent the logical grouping areas. + +
+ Profile Editor Update +
+ +
+ +*Note*: Display the value for the `FormGroup` in the component template using the `value` property and the `JsonPipe`. + +
+ +## Partial model updates + +When updating the value for a `FormGroup` that contains multiple controls, you may only want to update parts of the model instead of replacing its entire value. This section covers how to update specific parts of an `AbstractControl` model. + +### Patch the model value + +With a single control, you used the `setValue()` method to set the new value for an individual control. The `setValue()` method is more strict about adhering to the structure of the `FormGroup` and replaces the entire value for the control. The `patchValue()` method is more forgiving; it only replaces properties defined in the object that have changed in the form model, because you’re only providing partial updates. The strict checks in `setValue()` help catch errors in the nesting of complex forms, while `patchValue()` will fail silently in those cases. + +In the `ProfileEditorComponent`, the `updateProfile` method with the following example below to update the first name and street address for the user. + + + + + +Simulate an update by adding a button to the template to update the user profile on demand. + + + + + +When the button is clicked, the `profileForm` model is updated with just the `firstName` and `street` being modified. Notice that the `street` is provided in an object inside the `address` property. This is necessary because the `patchValue()` method applies the update against the model structure. `PatchValue()` only updates properties that the form model defines. + +## Generating form controls with `FormBuilder` + +Creating multiple form control instances manually can become very repetitive when dealing with multiple forms. The `FormBuilder` service provides convenience methods to handle generating controls. Underneath, the `FormBuilder` is creating and returning the instances in the same manner, but with much less work. The following section refactors the `ProfileEditor` component to use the `FormBuilder` instead of creating each `FormControl` and `FormGroup` by hand. + +### Step 1 - Import the `FormBuilder` class + +To use the `FormBuilder` service, import its class from the `@angular/forms` package. + + + + + +### Step 2 - Inject the `FormBuilder` service + +The FormBuilder is an injectable service that is provided with the `ReactiveFormsModule`. Inject this dependency by adding it to the component constructor. + + + + + +### Step 3 - Generate form controls + +The `FormBuilder` service has three methods: `control()`, `group()`, and `array()`. These methods are factory methods for generating form controls in your component class including a `FormControl`, `FormGroup`, and `FormArray` respectively. + +Replace the creation of the `profileForm` by using the `group` method to create the controls. + + + + + +In the example above, you use the `group()` method with the same names to define the properties in the model. Here, the value for each control name is an array containing the initial value as the first item. + +
+ +*Note*: You can define the control with just the initial value, but if your controls need sync or async validation, add sync and async validators as the second and third items in the array. + +
+ +Compare the two paths to achieve the same result. + + + + + + + + + + + + + +## Simple form validation + +Form validation is necessary when receiving user input through forms. This section covers adding a single validator to a form control and displaying the overall form status. Form validation is covered more extensively in the [Form Validation](guide/form-validation) guide. + +### Step 1 - Import a validator function + +Reactive forms include a set of validator functions out of the box for common use cases. These functions receive a control to validate against and return an error object or null based on the validation check. + +Import the `Validators` class from the `@angular/forms` package. + + + + + +### Step 2 - Make a field required + +The most common validation is making a field required. This section describes how to add a required validation to the `firstName` control. + +In the `ProfileEditor` component, add the `Validators.required` static method as the second item in the array for the `firstName` control. + + + + + +HTML5 has a set of built-in attributes that can be used for native validation, including `required`, `minlength`, `maxlength`, and more. Although _optional_, you can take advantage of these as additional attributes on your form input elements. Add the `required` attribute to the `firstName` input element. + + + + + +
+ +*Note:* These HTML5 validation attributes should be used _in combination with_ the built-in validators provided by Angular's reactive forms. Using these two validation practices in combination prevents errors about the expression being changed after the template has been checked. + +
+ +### Display form status + +Now that you’ve added a required field to the form control, its initial status is invalid. This invalid status propagates to the parent `FormGroup`, making its status invalid. You have access to the current status of the `FormGroup` through the `status` property on the instance. + +Display the current status of the `profileForm` using interpolation. + + + + + +
+ Profile Editor Validation +
+ +The submit button is disabled because the `profileForm` is invalid due to the required `firstName` form control. After you fill out the `firstName` input, the form becomes valid and the submit button is enabled. + +For more on form validation, visit the [Form Validation](guide/form-validation) guide. + +## Dynamic controls using form arrays + +A `FormArray` is an alternative to a `FormGroup` for managing any number of unnamed controls. As with `FormGroup` instances, you can dynamically insert and remove controls from a `FormArray`, and the `FormArray` instance's value and validation status is calculated from its child controls. However, you don't need to define a key for each control by name, so this is a great option if you don't know the number of child values in advance. The following example shows you how to manage an array of _aliases_ in the `ProfileEditor`. + +### Step 1 - Import the `FormArray` + +Import the `FormArray` class from `@angular/forms` to use for type information. The `FormBuilder` service is ready to create a `FormArray` instance. + + + + + +### Step 2 - Define a `FormArray` + +You can initialize a `FormArray` with any number of controls, from zero to many, by defining them in an array. Add an `aliases` property to the `FormGroup` for the `profileForm` to define the `FormArray`. + +Use the `FormBuilder.array()` method to define the array, and the `FormBuilder.control()` method to populate the array with an initial control. + + + + + +The _aliases_ control in the `FormGroup` is now populated with a single control until more are added dynamically. + +### Step 3 - Access the `FormArray` control + +Because a `FormArray` represents an undefined number of controls in array, accessing the control through a getter provides convenience and reusability. Use the _getter_ syntax to create an _aliases_ class property to retrieve the alias's `FormArray` control from the parent `FormGroup`. + + + + + +The getter provides easy access to the aliases `FormArray` instead of repeating the `profileForm.get()` method to get the instance. + +
+ +*Note*: Because the returned control is of type `AbstractControl`, you provide an explicit type to access the `FormArray` specific syntax for the methods. + +
+ +Define a method to dynamically insert an alias control into the alias's `FormArray`. The `FormArray.push()` method inserts the control as a new item in the array. + + + + + +In the template, the controls are iterated over to display each control as a separate input field. + +### Step 4 - Display the form array in the template + +After you define the aliases `FormArray` in your model, you must add it to the template for user input. Similar to the `formGroupName` input provided by the `FormGroupNameDirective`, a `formArrayName` binds communication from the `FormArray` to the template with the `FormArrayNameDirective`. + +Add the template HTML below after the closing `formGroupName` `
` element. + + + + + +The `*ngFor` directive iterates over each `FormControl` provided by the aliases `FormArray`. Because `FormArray` elements are unnamed, you assign the _index_ to the `i` variable and pass it to each control to bind it to the `formControlName` input. + +
+ Profile Editor Aliases +
+ +Each time a new `alias` is added, the `FormArray` is provided its control based on the index. This allows you to track each individual control when calculating the status and value of the root control. + +#### Add an Alias + +Initially, the form only contains one `Alias` field. Click the `Add Alias` button, and another field appears. You can also validate the array of aliases reported by the form model displayed by the `Form Value` at the bottom of the template. + +
+ +*Note*: Instead of a `FormControl` for each alias, you could compose another `FormGroup` with additional fields. The process of defining a control for each item is the same. + +
+ +{@a appendix} + +## Appendix + +{@a reactive-forms-api} + +### Reactive forms API + +Listed below are the base classes and services used to create and manage form controls. + +#### Classes @@ -279,9 +483,7 @@ This guide uses four fundamental classes to build a reactive form: @@ -295,9 +497,7 @@ It provides their common behaviors and properties. @@ -311,10 +511,7 @@ It corresponds to an HTML form control such as an `` or ` @@ -328,356 +525,38 @@ The top-level form in your component is a `FormGroup`. + + + + + + + +
- [`AbstractControl`](api/forms/AbstractControl "API Reference: FormControl") is the abstract base class for the three concrete form control classes; -`FormControl`, `FormGroup`, and `FormArray`. -It provides their common behaviors and properties. + The abstract base class for the three concrete form control classes; `FormControl`, `FormGroup`, and `FormArray`. It provides their common behaviors and properties. - [`FormControl`](api/forms/FormControl "API Reference: FormControl") -tracks the value and validity status of an individual form control. -It corresponds to an HTML form control such as an `` or `` or ` - [`FormGroup`](api/forms/FormGroup "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`. + Manages 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/FormArray "API Reference: FormArray") -tracks the value and validity state of a numerically indexed array of `AbstractControl` instances. + Manages the value and validity state of a numerically indexed array of `AbstractControl` instances.
+ FormBuilder + + + An injectable service that provides factory methods for creating control instances. + +
+When importing the `ReactiveFormsModule`, you also gain access to directives to use in your templates for binding the data model to the forms declaratively. -## Style the app +#### Directives -To use the bootstrap CSS classes that are in the template HTML of both the `AppComponent` and the `HeroDetailComponent`, -add the `bootstrap` CSS stylesheet to the head of `styles.css`: - - - - - - -Now that everything is wired up, serve the app with: - - - - ng serve - - - -The browser should display something like this: - - -
- Single FormControl -
- -{@a formgroup} - -## Add a FormGroup -Usually, if you have multiple `FormControls`, you register -them within a parent `FormGroup`. -To add a `FormGroup`, add it to the imports section -of `hero-detail.component.ts`: - - - - - -In the class, wrap the `FormControl` in a `FormGroup` called `heroForm` as follows: - - - - - -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. - - - - - -Notice that now the single `` is in a `
` element. - -`formGroup` is a reactive form directive that takes an existing -`FormGroup` instance and associates it with an HTML element. -In this case, it associates the `FormGroup` you saved as -`heroForm` with the `` element. - -Because the class now has a `FormGroup`, you must update the template -syntax for associating the `` with the corresponding -`FormControl` in the component class. -Without a parent `FormGroup`, -`[formControl]="name"` worked earlier because that directive -can stand alone, that is, it works without being in a `FormGroup`. -With a parent `FormGroup`, the `name` `` needs the syntax -`formControlName=name` in order to be associated -with the correct `FormControl` -in the class. This syntax tells Angular to look for the parent -`FormGroup`, in this case `heroForm`, and then _inside_ that group -to look for a `FormControl` called `name`. - - -{@a json} - -## Taking a look at the form model - -When the user enters data into an ``, the value -goes into the **_form model_**. -To see the form model, add the following line after the -closing `` tag in the `hero-detail.component.html`: - - - - - - - -The `heroForm.value` returns the _form model_. -Piping it through the `JsonPipe` renders the model as JSON in the browser: - - -
- JSON output -
- -The initial `name` property value is the empty string. -Type into the name `` and watch the keystrokes appear in the JSON. - -In real life apps, forms get big fast. -`FormBuilder` makes form development and maintenance easier. - - -{@a formbuilder} - -## Introduction to `FormBuilder` - -The `FormBuilder` class helps reduce repetition and -clutter by handling details of control creation for you. - -To use `FormBuilder`, import it into `hero-detail.component.ts`. You can remove `FormControl`: - - - - - -Use it to refactor the `HeroDetailComponent` into something that's 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: - - - - - - - -`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 your code more compact and readable because you don't have to write repeated `new FormControl(...)` statements. - -{@a validators} - -### `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. - - - - - - - -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`. - - - - - - - - -
- -Reactive validators are simple, composable functions. -Configuring validation is different in template-driven forms in that you must wrap validators in a directive. - -
- -Update the diagnostic message at the bottom of the template to display the form's validity status. - - - - - -The browser displays the following: - -
- Single FormControl -
- -`Validators.required` is working. The status is `INVALID` because the `` has no value. -Type into the `` to see the status change from `INVALID` to `VALID`. - -In a real app, you'd replace the diagnosic message with a user-friendly experience. - -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](guide/form-validation) guide. - -### More `FormControl`s - -This section adds additional `FormControl`s for the address, a super power, and a sidekick. - -Additionally, the address has a state property. The user will select a state with a `` elements, a ``. - -To make this change visually obvious, add an `

` header near the top with the text, _Secret Lair_. -The new address HTML looks like this: - - - - - - - -After these changes, the JSON output in the browser shows the revised form model -with the nested address `FormGroup`: - -
- JSON output -
- -This shows that the template -and the form model are talking to one another. - -{@a properties} - -## Inspect `FormControl` Properties - -You can inspect an individual `FormControl` within a form by extracting it with the `get()` method. -You can do this within the component class or display it on the -page by adding the following to the template, -immediately after the `{{form.value | json}}` interpolation as follows: - - - - - -To get the state of a `FormControl` that’s inside a `FormGroup`, use dot notation to traverse to the control. - - - - - -
- -*Note*: If you're coding along, remember to remove this reference to `address.street` when you get to the section on `FormArray`. In that section, you change the name of address in the component class and it will throw an error if you leave it in the template. - -
- -You can use this technique to display any property of a `FormControl` -such as one of the following: - - - - - - - - - - - - - - +
- - - - + + + + + + + + @@ -748,638 +637,46 @@ such as one of the following:
- Property + Directive @@ -688,59 +567,69 @@ such as one of the following:
- myControl.value + + FormControlDirective + Syncs a standalone `FormControl` instance to a form control element. - the value of a `FormControl`.
- myControl.status + + FormControlName + Syncs a `FormControl` in an existing `FormGroup` to a form control element by name. - the validity of a `FormControl`. Possible values: `VALID`, - `INVALID`, `PENDING`, or `DISABLED`.
- myControl.pristine + + FormGroupDirective + Syncs an existing `FormGroup` to a DOM element. - `true` if the user has _not_ changed the value in the UI. - Its opposite is `myControl.dirty`.
- myControl.untouched + + FormGroupName + Syncs a nested `FormGroup` to a DOM element. - `true` if the control user has not yet entered the HTML control - and triggered its blur event. Its opposite is `myControl.touched`. +
+ FormArrayName + + + Syncs a nested `FormArray` to a DOM element.
+### Comparison to template-driven forms +_Template-driven_ forms, introduced in the [Template-driven forms guide](guide/forms), take a completely different approach. -Read about other `FormControl` properties in the -[_AbstractControl_](api/forms/AbstractControl) API reference. +* You place HTML form controls (such as `` and ``. -You should see a new name in the log after each keystroke. - -### When to use it - -An interpolation binding is the easier way to display a name change. -Subscribing to an observable `FormControl` property is handy for triggering -application logic within the component class. - -{@a save} - -## Save form data - -The `HeroDetailComponent` captures user input but it doesn't do anything with it. -In a real app, you'd probably save those hero changes, revert unsaved changes, and resume editing. -After you implement both features in this section, the form will look like this: - - -
- Form with save & revert buttons -
- - - -### Save -When the user submits the form, -the `HeroDetailComponent` will pass an instance of the hero _data model_ -to a save method on the injected `HeroService`. Add the following to `HeroDetailComponent`. - - - - - - - -This original `hero` had the pre-save values. The user's changes are still in the _form model_. -So you create a new `hero` from a combination of original hero values (the `hero.id`) -and deep copies of the changed form model values, using the `prepareSaveHero()` helper. - - - - - - -Make sure to import `HeroService` and add it to the constructor: - - - - - - - - - -
- -**Address deep copy** - -Had you assigned the `formModel.secretLairs` to `saveHero.addresses` (see line commented out), -the addresses in `saveHero.addresses` array would be the same objects -as the lairs in the `formModel.secretLairs`. -A user's subsequent changes to a lair street would mutate an address street in the `saveHero`. - -The `prepareSaveHero` method makes copies of the form model's `secretLairs` objects so that can't happen. - - -
- - - -### Revert (cancel changes) -The user cancels changes and reverts the form to the original state by pressing the Revert button. - -Reverting is easy. Simply re-execute the `rebuildForm()` method that built the form model from the original, unchanged `hero` data model. - - - - - - - -### Buttons -Add the "Save" and "Revert" buttons near the top of the component's template: - - - - - - - -The buttons are disabled until the user "dirties" the form by changing a value in any of its form controls (`heroForm.dirty`). - -Clicking a button of type `"submit"` triggers the `ngSubmit` event which calls the component's `onSubmit` method. -Clicking the revert button triggers a call to the component's `revert` method. -Users now can save or revert changes. - -Try the . - -{@a source-code} - - -The key files of the final version are as follows: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -You can download the complete source for all steps in this guide -from the Reactive Forms Demo live example. diff --git a/aio/content/images/guide/reactive-forms/address-group.png b/aio/content/images/guide/reactive-forms/address-group.png deleted file mode 100644 index ee1587c107..0000000000 Binary files a/aio/content/images/guide/reactive-forms/address-group.png and /dev/null differ diff --git a/aio/content/images/guide/reactive-forms/addresses-array.png b/aio/content/images/guide/reactive-forms/addresses-array.png deleted file mode 100644 index b86733613e..0000000000 Binary files a/aio/content/images/guide/reactive-forms/addresses-array.png and /dev/null differ diff --git a/aio/content/images/guide/reactive-forms/hero-detail.png b/aio/content/images/guide/reactive-forms/hero-detail.png deleted file mode 100644 index 1079d3dee3..0000000000 Binary files a/aio/content/images/guide/reactive-forms/hero-detail.png and /dev/null differ diff --git a/aio/content/images/guide/reactive-forms/hero-list.png b/aio/content/images/guide/reactive-forms/hero-list.png deleted file mode 100644 index 151eee981f..0000000000 Binary files a/aio/content/images/guide/reactive-forms/hero-list.png and /dev/null differ diff --git a/aio/content/images/guide/reactive-forms/json-output.png b/aio/content/images/guide/reactive-forms/json-output.png deleted file mode 100644 index 11f4e77fd9..0000000000 Binary files a/aio/content/images/guide/reactive-forms/json-output.png and /dev/null differ diff --git a/aio/content/images/guide/reactive-forms/just-formcontrol.png b/aio/content/images/guide/reactive-forms/just-formcontrol.png deleted file mode 100644 index d300239a2f..0000000000 Binary files a/aio/content/images/guide/reactive-forms/just-formcontrol.png and /dev/null differ diff --git a/aio/content/images/guide/reactive-forms/name-editor-1.png b/aio/content/images/guide/reactive-forms/name-editor-1.png new file mode 100644 index 0000000000..06ef679f5d Binary files /dev/null and b/aio/content/images/guide/reactive-forms/name-editor-1.png differ diff --git a/aio/content/images/guide/reactive-forms/name-editor-2.png b/aio/content/images/guide/reactive-forms/name-editor-2.png new file mode 100644 index 0000000000..e94e4a1961 Binary files /dev/null and b/aio/content/images/guide/reactive-forms/name-editor-2.png differ diff --git a/aio/content/images/guide/reactive-forms/profile-editor-1.png b/aio/content/images/guide/reactive-forms/profile-editor-1.png new file mode 100644 index 0000000000..e9cf0de511 Binary files /dev/null and b/aio/content/images/guide/reactive-forms/profile-editor-1.png differ diff --git a/aio/content/images/guide/reactive-forms/profile-editor-2.png b/aio/content/images/guide/reactive-forms/profile-editor-2.png new file mode 100644 index 0000000000..06396d10de Binary files /dev/null and b/aio/content/images/guide/reactive-forms/profile-editor-2.png differ diff --git a/aio/content/images/guide/reactive-forms/profile-editor-3.png b/aio/content/images/guide/reactive-forms/profile-editor-3.png new file mode 100644 index 0000000000..55f995737e Binary files /dev/null and b/aio/content/images/guide/reactive-forms/profile-editor-3.png differ diff --git a/aio/content/images/guide/reactive-forms/profile-editor-4.png b/aio/content/images/guide/reactive-forms/profile-editor-4.png new file mode 100644 index 0000000000..9de5521146 Binary files /dev/null and b/aio/content/images/guide/reactive-forms/profile-editor-4.png differ diff --git a/aio/content/images/guide/reactive-forms/save-revert-buttons.png b/aio/content/images/guide/reactive-forms/save-revert-buttons.png deleted file mode 100644 index 6c9d0c55a7..0000000000 Binary files a/aio/content/images/guide/reactive-forms/save-revert-buttons.png and /dev/null differ diff --git a/aio/content/images/guide/reactive-forms/validators-json-output.png b/aio/content/images/guide/reactive-forms/validators-json-output.png deleted file mode 100644 index aca7c0450d..0000000000 Binary files a/aio/content/images/guide/reactive-forms/validators-json-output.png and /dev/null differ