docs: add example and edit two-way-binding section of Template Syntax (#26278)
PR Close #26278
This commit is contained in:
parent
85d38ae564
commit
7e3a60ad31
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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));
|
|
@ -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}
|
||||
|
||||
## 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/>
|
||||
|
||||
|
|
Loading…
Reference in New Issue