docs: add example and edit two-way-binding section of Template Syntax (#26278)

PR Close #26278
This commit is contained in:
Kapunahele Wong 2018-10-03 15:49:05 -04:00 committed by Andrew Kushnir
parent 85d38ae564
commit 7e3a60ad31
16 changed files with 256 additions and 21 deletions

View File

@ -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();
}
}

View File

@ -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');
});
});

View File

@ -0,0 +1,17 @@
<h1 id="two-way">Two-way Binding</h1>
<div id="two-way-1">
<!-- #docregion two-way-1 -->
<app-sizer [(size)]="fontSizePx"></app-sizer>
<div [style.font-size.px]="fontSizePx">Resizable Text</div>
<!-- #enddocregion two-way-1 -->
<label>FontSize (px): <input [(ngModel)]="fontSizePx"></label>
</div>
<br>
<div id="two-way-2">
<h2>De-sugared two-way binding</h2>
<!-- #docregion two-way-2 -->
<app-sizer [size]="fontSizePx" (sizeChange)="fontSizePx=$event"></app-sizer>
<!-- #enddocregion two-way-2 -->
</div>

View File

@ -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!');
}));
});

View File

@ -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
}

View File

@ -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 { }

View File

@ -0,0 +1,5 @@
<div>
<button (click)="dec()" title="smaller">-</button>
<button (click)="inc()" title="bigger">+</button>
<label [style.font-size.px]="size">FontSize: {{size}}px</label>
</div>

View File

@ -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<SizerComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ SizerComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SizerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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<number>();
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);
}
}

View File

@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Two-way Binding</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
</body>
</html>

View File

@ -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));

View File

@ -0,0 +1,10 @@
{
"description": "Two-way binding",
"files": [
"!**/*.d.ts",
"!**/*.js",
"!**/*.[1,2].*"
],
"file": "src/app/app.component.ts",
"tags": ["Two-way binding"]
}

View File

@ -1157,16 +1157,23 @@ These changes propagate through the system and ultimately display in this and ot
{@a two-way}
## Two-way binding ( <span class="syntax">[(...)]</span> )
## 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 <live-example name="two-way-binding">two-way binding example</live-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, `()`.
<div class="callout is-important">
@ -1178,44 +1185,52 @@ Visualize a *banana in a box* to remember that the parentheses go _inside_ the b
</div>
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:
<code-example path="template-syntax/src/app/sizer.component.ts" header="src/app/sizer.component.ts">
<code-example path="two-way-binding/src/app/sizer/sizer.component.ts" header="src/app/sizer.component.ts" linenums="false">
</code-example>
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`:
<code-example path="template-syntax/src/app/app.component.html" linenums="false" header="src/app/app.component.html (two-way-1)" region="two-way-1">
<code-example path="two-way-binding/src/app/app.component.html" linenums="false" header="src/app/app.component.html (two-way-1)" region="two-way-1">
</code-example>
The `AppComponent.fontSizePx` establishes the initial `SizerComponent.size` value.
<code-example path="two-way-binding/src/app/app.component.ts" header="src/app/app.component.ts" region="font-size">
</code-example>
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:
<code-example path="template-syntax/src/app/app.component.html" linenums="false" header="src/app/app.component.html (two-way-2)" region="two-way-2">
<code-example path="two-way-binding/src/app/app.component.html" linenums="false" header="src/app/app.component.html (two-way-2)" region="two-way-2">
</code-example>
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 `<input>` and `<select>`.
However, no native HTML element follows the `x` value and `xChange` event pattern.
Fortunately, the Angular [_NgModel_](guide/template-syntax#ngModel) directive is a bridge that enables two-way binding to form elements.
The two-way binding syntax is a great convenience compared to
separate property and event bindings. It would be convenient to
use two-way binding with HTML form elements like `<input>` and
`<select>`. However, no native HTML element follows the `x`
value and `xChange` event pattern.
For more on how to use two-way binding in forms, see
Angular [NgModel](guide/template-syntax#ngModel).
<hr/>