docs: add example and edit two-way-binding section of Template Syntax (#26278)
PR Close #26278
This commit is contained in:

committed by
Andrew Kushnir

parent
85d38ae564
commit
7e3a60ad31
11
aio/content/examples/two-way-binding/e2e/app.po.ts
Normal file
11
aio/content/examples/two-way-binding/e2e/app.po.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
42
aio/content/examples/two-way-binding/e2e/src/app.e2e-spec.ts
Normal file
42
aio/content/examples/two-way-binding/e2e/src/app.e2e-spec.ts
Normal 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');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -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>
|
||||||
|
|
||||||
|
|
@ -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!');
|
||||||
|
}));
|
||||||
|
});
|
@ -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
|
||||||
|
}
|
22
aio/content/examples/two-way-binding/src/app/app.module.ts
Normal file
22
aio/content/examples/two-way-binding/src/app/app.module.ts
Normal 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 { }
|
@ -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>
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
14
aio/content/examples/two-way-binding/src/index.html
Normal file
14
aio/content/examples/two-way-binding/src/index.html
Normal 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>
|
12
aio/content/examples/two-way-binding/src/main.ts
Normal file
12
aio/content/examples/two-way-binding/src/main.ts
Normal 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));
|
10
aio/content/examples/two-way-binding/stackblitz.json
Normal file
10
aio/content/examples/two-way-binding/stackblitz.json
Normal 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"]
|
||||||
|
}
|
@ -1157,16 +1157,23 @@ These changes propagate through the system and ultimately display in this and ot
|
|||||||
|
|
||||||
{@a two-way}
|
{@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
|
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>.
|
||||||
and listening for an element change event.
|
|
||||||
|
|
||||||
Angular offers a special _two-way data binding_ syntax for this purpose, **`[(x)]`**.
|
### Basics of two-way binding
|
||||||
The `[(x)]` syntax combines the brackets
|
|
||||||
of _property binding_, `[x]`, with the parentheses of _event binding_, `(x)`.
|
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">
|
<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>
|
</div>
|
||||||
|
|
||||||
The `[(x)]` syntax is easy to demonstrate when the element has a settable property called `x`
|
The `[()]` syntax is easy to demonstrate when the element has a settable
|
||||||
and a corresponding event named `xChange`.
|
property called `x` and a corresponding event named `xChange`.
|
||||||
Here's a `SizerComponent` that fits the pattern.
|
Here's a `SizerComponent` that fits this pattern.
|
||||||
It has a `size` value property and a companion `sizeChange` event:
|
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>
|
</code-example>
|
||||||
|
|
||||||
The initial `size` is an input value from a property binding.
|
The initial `size` is an input value from a property binding.
|
||||||
Clicking the buttons increases or decreases the `size`, within min/max values constraints,
|
Clicking the buttons increases or decreases the `size`, within
|
||||||
and then raises (_emits_) the `sizeChange` event with the adjusted size.
|
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`:
|
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>
|
</code-example>
|
||||||
|
|
||||||
The `AppComponent.fontSizePx` establishes the initial `SizerComponent.size` value.
|
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.
|
Clicking the buttons updates the `AppComponent.fontSizePx` via the two-way binding.
|
||||||
The revised `AppComponent.fontSizePx` value flows through to the _style_ binding,
|
The revised `AppComponent.fontSizePx` value flows through to the _style_ binding,
|
||||||
making the displayed text bigger or smaller.
|
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.
|
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>
|
</code-example>
|
||||||
|
|
||||||
The `$event` variable contains the payload of the `SizerComponent.sizeChange` event.
|
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.
|
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>`.
|
The two-way binding syntax is a great convenience compared to
|
||||||
However, no native HTML element follows the `x` value and `xChange` event pattern.
|
separate property and event bindings. It would be convenient to
|
||||||
|
use two-way binding with HTML form elements like `<input>` and
|
||||||
Fortunately, the Angular [_NgModel_](guide/template-syntax#ngModel) directive is a bridge that enables two-way binding to form elements.
|
`<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/>
|
<hr/>
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user