diff --git a/aio/content/examples/two-way-binding/e2e/app.po.ts b/aio/content/examples/two-way-binding/e2e/app.po.ts new file mode 100644 index 0000000000..82ea75ba50 --- /dev/null +++ b/aio/content/examples/two-way-binding/e2e/app.po.ts @@ -0,0 +1,11 @@ +import { browser, by, element } from 'protractor'; + +export class AppPage { + navigateTo() { + return browser.get('/'); + } + + getParagraphText() { + return element(by.css('app-root h1')).getText(); + } +} diff --git a/aio/content/examples/two-way-binding/e2e/src/app.e2e-spec.ts b/aio/content/examples/two-way-binding/e2e/src/app.e2e-spec.ts new file mode 100644 index 0000000000..524ae79f5e --- /dev/null +++ b/aio/content/examples/two-way-binding/e2e/src/app.e2e-spec.ts @@ -0,0 +1,42 @@ +import { browser, element, by } from 'protractor'; + +describe('Two-way binding e2e tests', () => { + + beforeEach(function () { + browser.get(''); + }); + + let minusButton = element.all(by.css('button')).get(0); + let plusButton = element.all(by.css('button')).get(1); + let minus2Button = element.all(by.css('button')).get(2); + let plus2Button = element.all(by.css('button')).get(3); + + it('should display Two-way Binding', function () { + expect(element(by.css('h1')).getText()).toEqual('Two-way Binding'); + }); + + it('should display four buttons', function() { + expect(minusButton.getText()).toBe('-'); + expect(plusButton.getText()).toBe('+'); + expect(minus2Button.getText()).toBe('-'); + expect(plus2Button.getText()).toBe('+'); + }); + + it('should change font size labels', async () => { + await minusButton.click(); + expect(element.all(by.css('label')).get(0).getText()).toEqual('FontSize: 15px'); + expect(element.all(by.css('input')).get(0).getAttribute('value')).toEqual('15'); + + await plusButton.click(); + expect(element.all(by.css('label')).get(0).getText()).toEqual('FontSize: 16px'); + expect(element.all(by.css('input')).get(0).getAttribute('value')).toEqual('16'); + + await minus2Button.click(); + await expect(element.all(by.css('label')).get(2).getText()).toEqual('FontSize: 15px'); + }); + + it('should display De-sugared two-way binding', function () { + expect(element(by.css('h2')).getText()).toEqual('De-sugared two-way binding'); + }); + +}); diff --git a/aio/content/examples/two-way-binding/example-config.json b/aio/content/examples/two-way-binding/example-config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/two-way-binding/src/app/app.component.css b/aio/content/examples/two-way-binding/src/app/app.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/two-way-binding/src/app/app.component.html b/aio/content/examples/two-way-binding/src/app/app.component.html new file mode 100644 index 0000000000..f96bd38688 --- /dev/null +++ b/aio/content/examples/two-way-binding/src/app/app.component.html @@ -0,0 +1,17 @@ +

Two-way Binding

+
+ + +
Resizable Text
+ + +
+
+
+

De-sugared two-way binding

+ + + +
+ + diff --git a/aio/content/examples/two-way-binding/src/app/app.component.spec.ts b/aio/content/examples/two-way-binding/src/app/app.component.spec.ts new file mode 100644 index 0000000000..bcbdf36b3e --- /dev/null +++ b/aio/content/examples/two-way-binding/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 app!'); + })); +}); diff --git a/aio/content/examples/two-way-binding/src/app/app.component.ts b/aio/content/examples/two-way-binding/src/app/app.component.ts new file mode 100644 index 0000000000..34a69e083f --- /dev/null +++ b/aio/content/examples/two-way-binding/src/app/app.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] +}) +export class AppComponent { + constructor() { } + // #docregion font-size + fontSizePx = 16; + // #enddocregion font-size +} diff --git a/aio/content/examples/two-way-binding/src/app/app.module.ts b/aio/content/examples/two-way-binding/src/app/app.module.ts new file mode 100644 index 0000000000..c6fd704599 --- /dev/null +++ b/aio/content/examples/two-way-binding/src/app/app.module.ts @@ -0,0 +1,22 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; + + +import { AppComponent } from './app.component'; +import { SizerComponent } from './sizer/sizer.component'; +import { FormsModule } from '@angular/forms'; + + +@NgModule({ + declarations: [ + AppComponent, + SizerComponent + ], + imports: [ + BrowserModule, + FormsModule + ], + providers: [], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git a/aio/content/examples/two-way-binding/src/app/sizer/sizer.component.css b/aio/content/examples/two-way-binding/src/app/sizer/sizer.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/two-way-binding/src/app/sizer/sizer.component.html b/aio/content/examples/two-way-binding/src/app/sizer/sizer.component.html new file mode 100644 index 0000000000..c635935aa0 --- /dev/null +++ b/aio/content/examples/two-way-binding/src/app/sizer/sizer.component.html @@ -0,0 +1,5 @@ +
+ + + +
diff --git a/aio/content/examples/two-way-binding/src/app/sizer/sizer.component.spec.ts b/aio/content/examples/two-way-binding/src/app/sizer/sizer.component.spec.ts new file mode 100644 index 0000000000..3c3a3ada05 --- /dev/null +++ b/aio/content/examples/two-way-binding/src/app/sizer/sizer.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SizerComponent } from './sizer.component'; + +describe('SizerComponent', () => { + let component: SizerComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ SizerComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SizerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/aio/content/examples/two-way-binding/src/app/sizer/sizer.component.ts b/aio/content/examples/two-way-binding/src/app/sizer/sizer.component.ts new file mode 100644 index 0000000000..b9c8f670e2 --- /dev/null +++ b/aio/content/examples/two-way-binding/src/app/sizer/sizer.component.ts @@ -0,0 +1,22 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core'; + +@Component({ + selector: 'app-sizer', + templateUrl: './sizer.component.html', + styleUrls: ['./sizer.component.css'] +}) +export class SizerComponent { + + + @Input() size: number | string; + @Output() sizeChange = new EventEmitter(); + + dec() { this.resize(-1); } + inc() { this.resize(+1); } + + resize(delta: number) { + this.size = Math.min(40, Math.max(8, +this.size + delta)); + this.sizeChange.emit(this.size); + } + +} diff --git a/aio/content/examples/two-way-binding/src/index.html b/aio/content/examples/two-way-binding/src/index.html new file mode 100644 index 0000000000..74a6a34c64 --- /dev/null +++ b/aio/content/examples/two-way-binding/src/index.html @@ -0,0 +1,14 @@ + + + + + Two-way Binding + + + + + + + + + diff --git a/aio/content/examples/two-way-binding/src/main.ts b/aio/content/examples/two-way-binding/src/main.ts new file mode 100644 index 0000000000..91ec6da5f0 --- /dev/null +++ b/aio/content/examples/two-way-binding/src/main.ts @@ -0,0 +1,12 @@ +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) + .catch(err => console.log(err)); diff --git a/aio/content/examples/two-way-binding/stackblitz.json b/aio/content/examples/two-way-binding/stackblitz.json new file mode 100644 index 0000000000..9128372239 --- /dev/null +++ b/aio/content/examples/two-way-binding/stackblitz.json @@ -0,0 +1,10 @@ +{ + "description": "Two-way binding", + "files": [ + "!**/*.d.ts", + "!**/*.js", + "!**/*.[1,2].*" + ], + "file": "src/app/app.component.ts", + "tags": ["Two-way binding"] +} diff --git a/aio/content/guide/template-syntax.md b/aio/content/guide/template-syntax.md index f07416384e..419fde5d9a 100644 --- a/aio/content/guide/template-syntax.md +++ b/aio/content/guide/template-syntax.md @@ -1157,16 +1157,23 @@ These changes propagate through the system and ultimately display in this and ot {@a two-way} -## Two-way binding ( [(...)] ) +## Two-way binding `[(...)]` -You often want to both display a data property and update that property when the user makes changes. +Two-way binding gives your app a way to share data between a component class and +its template. -On the element side that takes a combination of setting a specific element property -and listening for an element change event. +For a demonstration of the syntax and code snippets in this section, see the two-way binding example. -Angular offers a special _two-way data binding_ syntax for this purpose, **`[(x)]`**. -The `[(x)]` syntax combines the brackets -of _property binding_, `[x]`, with the parentheses of _event binding_, `(x)`. +### Basics of two-way binding + +Two-way binding does two things: + +1. Sets a specific element property. +1. Listens for an element change event. + +Angular offers a special _two-way data binding_ syntax for this purpose, `[()]`. +The `[()]` syntax combines the brackets +of property binding, `[]`, with the parentheses of event binding, `()`.
@@ -1178,44 +1185,52 @@ Visualize a *banana in a box* to remember that the parentheses go _inside_ the b
-The `[(x)]` syntax is easy to demonstrate when the element has a settable property called `x` -and a corresponding event named `xChange`. -Here's a `SizerComponent` that fits the pattern. +The `[()]` syntax is easy to demonstrate when the element has a settable +property called `x` and a corresponding event named `xChange`. +Here's a `SizerComponent` that fits this pattern. It has a `size` value property and a companion `sizeChange` event: - + The initial `size` is an input value from a property binding. -Clicking the buttons increases or decreases the `size`, within min/max values constraints, -and then raises (_emits_) the `sizeChange` event with the adjusted size. +Clicking the buttons increases or decreases the `size`, within +min/max value constraints, +and then raises, or emits, the `sizeChange` event with the adjusted size. Here's an example in which the `AppComponent.fontSizePx` is two-way bound to the `SizerComponent`: - + The `AppComponent.fontSizePx` establishes the initial `SizerComponent.size` value. + + + + Clicking the buttons updates the `AppComponent.fontSizePx` via the two-way binding. The revised `AppComponent.fontSizePx` value flows through to the _style_ binding, making the displayed text bigger or smaller. The two-way binding syntax is really just syntactic sugar for a _property_ binding and an _event_ binding. -Angular _desugars_ the `SizerComponent` binding into this: +Angular desugars the `SizerComponent` binding into this: - + The `$event` variable contains the payload of the `SizerComponent.sizeChange` event. Angular assigns the `$event` value to the `AppComponent.fontSizePx` when the user clicks the buttons. -Clearly the two-way binding syntax is a great convenience compared to separate property and event bindings. +## Two-way binding in forms -It would be convenient to use two-way binding with HTML form elements like `` and `` and +`