diff --git a/aio/content/examples/event-binding/e2e/src/app.e2e-spec.ts b/aio/content/examples/event-binding/e2e/src/app.e2e-spec.ts new file mode 100644 index 0000000000..881a49f700 --- /dev/null +++ b/aio/content/examples/event-binding/e2e/src/app.e2e-spec.ts @@ -0,0 +1,71 @@ +'use strict'; // necessary for es6 output in node + +import { browser, element, by, protractor } from 'protractor'; + +describe('Event binding example', function () { + + beforeEach(function () { + browser.get(''); + }); + + let saveButton = element.all(by.css('button')).get(0); + let onSaveButton = element.all(by.css('button')).get(1); + let myClick = element.all(by.css('button')).get(2); + let deleteButton = element.all(by.css('button')).get(3); + let saveNoProp = element.all(by.css('button')).get(4); + let saveProp = element.all(by.css('button')).get(5); + + + it('should display Event Binding with Angular', function () { + expect(element(by.css('h1')).getText()).toEqual('Event Binding'); + }); + + it('should display 6 buttons', function() { + expect(saveButton.getText()).toBe('Save'); + expect(onSaveButton.getText()).toBe('on-click Save'); + expect(myClick.getText()).toBe('click with myClick'); + expect(deleteButton.getText()).toBe('Delete'); + expect(saveNoProp.getText()).toBe('Save, no propagation'); + expect(saveProp.getText()).toBe('Save with propagation'); + }); + + it('should support user input', function () { + let input = element(by.css('input')); + let bindingResult = element.all(by.css('h4')).get(1); + expect(bindingResult.getText()).toEqual('Result: teapot'); + input.sendKeys('abc'); + expect(bindingResult.getText()).toEqual('Result: teapotabc'); + }); + + it('should hide the item img', async () => { + let deleteButton = element.all(by.css('button')).get(3); + await deleteButton.click(); + browser.switchTo().alert().accept(); + expect(element.all(by.css('img')).get(0).getCssValue('display')).toEqual('none'); + }); + + it('should show two alerts', async () => { + let parentDiv = element.all(by.css('.parent-div')); + let childDiv = element.all(by.css('div > div')).get(1); + await parentDiv.click(); + browser.switchTo().alert().accept(); + expect(childDiv.getText()).toEqual('Click me too! (child)'); + await childDiv.click(); + expect(browser.switchTo().alert().getText()).toEqual('Click me. Event target class is child-div'); + browser.switchTo().alert().accept(); + }); + + it('should show 1 alert from Save, no prop, button', async () => { + await saveNoProp.click(); + expect(browser.switchTo().alert().getText()).toEqual('Saved. Event target is Save, no propagation'); + browser.switchTo().alert().accept(); + }); + + it('should show 2 alerts from Save w/prop button', async () => { + await saveProp.click(); + expect(browser.switchTo().alert().getText()).toEqual('Saved.'); + browser.switchTo().alert().accept(); + expect(browser.switchTo().alert().getText()).toEqual('Saved.'); + browser.switchTo().alert().accept(); + }); +}); diff --git a/aio/content/examples/event-binding/example-config.json b/aio/content/examples/event-binding/example-config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/event-binding/src/app/app.component.css b/aio/content/examples/event-binding/src/app/app.component.css new file mode 100644 index 0000000000..a80480a914 --- /dev/null +++ b/aio/content/examples/event-binding/src/app/app.component.css @@ -0,0 +1,25 @@ +.group { + background-color: #dae8f9; + padding: 1rem; + margin: 1rem 0; +} + +.parent-div { + background-color: #bdd1f7; + border: solid 1px rgb(25, 118, 210); + padding: 1rem; +} + +.parent-div:hover { + background-color: #8fb4f9; +} + +.child-div { + margin-top: 1rem; + background-color: #fff; + padding: 1rem; +} + +.child-div:hover { + background-color: #eee; +} diff --git a/aio/content/examples/event-binding/src/app/app.component.html b/aio/content/examples/event-binding/src/app/app.component.html new file mode 100644 index 0000000000..5edc3eaee2 --- /dev/null +++ b/aio/content/examples/event-binding/src/app/app.component.html @@ -0,0 +1,53 @@ +

Event Binding

+ +
+

Target event

+ + + + + + + + + +

myClick is an event on the custom ClickDirective:

+ + {{clickMessage}} + + +
+ +
+

$event and event handling statements

+

Result: {{currentItem.name}}

+ + + + without NgModel + +
+ +
+

Binding to a nested component

+

Custom events with EventEmitter

+ + + + + +

Click to see event target class:

+
Click me (parent) +
Click me too! (child)
+
+ +

Saves only once:

+
+ +
+ +

Saves twice:

+
+ +
diff --git a/aio/content/examples/event-binding/src/app/app.component.spec.ts b/aio/content/examples/event-binding/src/app/app.component.spec.ts new file mode 100644 index 0000000000..852c902d87 --- /dev/null +++ b/aio/content/examples/event-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 'Featured product:'`, async(() => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app.title).toEqual('Featured product:'); + })); + it('should render title in a p tag', async(() => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.debugElement.nativeElement; + expect(compiled.querySelector('p').textContent).toContain('Featured product:'); + })); +}); diff --git a/aio/content/examples/event-binding/src/app/app.component.ts b/aio/content/examples/event-binding/src/app/app.component.ts new file mode 100644 index 0000000000..2dc9515d40 --- /dev/null +++ b/aio/content/examples/event-binding/src/app/app.component.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +import { Item } from './item'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] +}) +export class AppComponent { + + currentItem = { name: 'teapot'} ; + clickMessage = ''; + + onSave(event: KeyboardEvent) { + const evtMsg = event ? ' Event target is ' + (event.target).textContent : ''; + alert('Saved.' + evtMsg); + if (event) { event.stopPropagation(); } + } + + deleteItem(item: Item) { + alert(`Delete the ${item}.`); + } + + onClickMe(event: KeyboardEvent) { + const evtMsg = event ? ' Event target class is ' + (event.target).className : ''; + alert('Click me.' + evtMsg); + } + +} diff --git a/aio/content/examples/event-binding/src/app/app.module.ts b/aio/content/examples/event-binding/src/app/app.module.ts new file mode 100644 index 0000000000..d331a1d8fc --- /dev/null +++ b/aio/content/examples/event-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 { ItemDetailComponent } from './item-detail/item-detail.component'; +import { ClickDirective } from './click.directive'; + + +@NgModule({ + declarations: [ + AppComponent, + ItemDetailComponent, + ClickDirective + ], + imports: [ + BrowserModule + ], + providers: [], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git a/aio/content/examples/event-binding/src/app/click.directive.ts b/aio/content/examples/event-binding/src/app/click.directive.ts new file mode 100644 index 0000000000..4e9e9085b1 --- /dev/null +++ b/aio/content/examples/event-binding/src/app/click.directive.ts @@ -0,0 +1,18 @@ +/* tslint:disable use-output-property-decorator directive-class-suffix */ +import { Directive, ElementRef, EventEmitter, Output } from '@angular/core'; + +@Directive({selector: '[myClick]'}) +export class ClickDirective { + @Output('myClick') clicks = new EventEmitter(); // @Output(alias) propertyName = ... + + toggle = false; + + constructor(el: ElementRef) { + el.nativeElement + .addEventListener('click', (event: Event) => { + this.toggle = !this.toggle; + this.clicks.emit(this.toggle ? 'Click!' : ''); + }); + } +} + diff --git a/aio/content/examples/event-binding/src/app/item-detail/item-detail.component.css b/aio/content/examples/event-binding/src/app/item-detail/item-detail.component.css new file mode 100644 index 0000000000..38789a8947 --- /dev/null +++ b/aio/content/examples/event-binding/src/app/item-detail/item-detail.component.css @@ -0,0 +1,11 @@ +.detail { + border: 1px solid rgb(25, 118, 210); + padding: 1rem; + margin: 1rem 0; +} + +img { + max-width: 100px; + display: block; + padding: 1rem 0; +} diff --git a/aio/content/examples/event-binding/src/app/item-detail/item-detail.component.html b/aio/content/examples/event-binding/src/app/item-detail/item-detail.component.html new file mode 100644 index 0000000000..0cd2c19720 --- /dev/null +++ b/aio/content/examples/event-binding/src/app/item-detail/item-detail.component.html @@ -0,0 +1,9 @@ +
+

This is the ItemDetailComponent

+ + + {{ item.name }} + + + +
diff --git a/aio/content/examples/event-binding/src/app/item-detail/item-detail.component.spec.ts b/aio/content/examples/event-binding/src/app/item-detail/item-detail.component.spec.ts new file mode 100644 index 0000000000..7559cb65f6 --- /dev/null +++ b/aio/content/examples/event-binding/src/app/item-detail/item-detail.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ItemDetailComponent } from './item-detail.component'; + +describe('ItemDetailComponent', () => { + let component: ItemDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ItemDetailComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ItemDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/aio/content/examples/event-binding/src/app/item-detail/item-detail.component.ts b/aio/content/examples/event-binding/src/app/item-detail/item-detail.component.ts new file mode 100644 index 0000000000..94ea032ce3 --- /dev/null +++ b/aio/content/examples/event-binding/src/app/item-detail/item-detail.component.ts @@ -0,0 +1,30 @@ +/* tslint:disable use-input-property-decorator use-output-property-decorator */ +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +import { Item } from '../item'; + +@Component({ + selector: 'app-item-detail', + styleUrls: ['./item-detail.component.css'], + templateUrl: './item-detail.component.html' +}) +export class ItemDetailComponent { + + @Input() item; + itemImageUrl = 'assets/teapot.svg'; + lineThrough = ''; + displayNone = ''; + @Input() prefix = ''; + + // #docregion deleteRequest + // This component makes a request but it can't actually delete a hero. + @Output() deleteRequest = new EventEmitter(); + + delete() { + this.deleteRequest.emit(this.item.name); + this.displayNone = this.displayNone ? '' : 'none'; + this.lineThrough = this.lineThrough ? '' : 'line-through'; + } + // #enddocregion deleteRequest + +} diff --git a/aio/content/examples/event-binding/src/app/item.ts b/aio/content/examples/event-binding/src/app/item.ts new file mode 100644 index 0000000000..1a80527e1c --- /dev/null +++ b/aio/content/examples/event-binding/src/app/item.ts @@ -0,0 +1,4 @@ +export class Item { + name: ''; +} + diff --git a/aio/content/examples/event-binding/src/assets/teapot.svg b/aio/content/examples/event-binding/src/assets/teapot.svg new file mode 100644 index 0000000000..b5f51cf030 --- /dev/null +++ b/aio/content/examples/event-binding/src/assets/teapot.svg @@ -0,0 +1 @@ + diff --git a/aio/content/examples/event-binding/src/index.html b/aio/content/examples/event-binding/src/index.html new file mode 100644 index 0000000000..37a728cf33 --- /dev/null +++ b/aio/content/examples/event-binding/src/index.html @@ -0,0 +1,14 @@ + + + + + EventBinding + + + + + + + + + diff --git a/aio/content/examples/event-binding/src/main.ts b/aio/content/examples/event-binding/src/main.ts new file mode 100644 index 0000000000..91ec6da5f0 --- /dev/null +++ b/aio/content/examples/event-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/event-binding/stackblitz.json b/aio/content/examples/event-binding/stackblitz.json new file mode 100644 index 0000000000..765446194d --- /dev/null +++ b/aio/content/examples/event-binding/stackblitz.json @@ -0,0 +1,10 @@ +{ + "description": "Event Binding", + "files": [ + "!**/*.d.ts", + "!**/*.js", + "!**/*.[1,2].*" + ], + "file": "src/app/app.component.ts", + "tags": ["Event Binding"] +} diff --git a/aio/content/guide/template-syntax.md b/aio/content/guide/template-syntax.md index 758ba8d8e7..4e177a3b85 100644 --- a/aio/content/guide/template-syntax.md +++ b/aio/content/guide/template-syntax.md @@ -797,7 +797,6 @@ content harmlessly.
- {@a other-bindings} ## Attribute, class, and style bindings @@ -944,56 +943,44 @@ Note that a _style property_ name can be written in either {@a event-binding} -## Event binding ( (event) ) +## Event binding `(event)` -The bindings directives you've met so far flow data in one direction: **from a component to an element**. +Event binding allows you to listen for certain events such as +keystrokes, mouse movements, clicks, and touches. For an example +demonstrating all of the points in this section, see the event binding example. -Users don't just stare at the screen. They enter text into input boxes. They pick items from lists. -They click buttons. Such user actions may result in a flow of data in the opposite direction: -**from an element to a component**. - -The only way to know about a user action is to listen for certain events such as -keystrokes, mouse movements, clicks, and touches. -You declare your interest in user actions through Angular event binding. - -Event binding syntax consists of a **target event** name +Angular event binding syntax consists of a **target event** name within parentheses on the left of an equal sign, and a quoted -[template statement](guide/template-syntax#template-statements) on the right. +template statement on the right. The following event binding listens for the button's click events, calling the component's `onSave()` method whenever a click occurs: - - +
+ Syntax diagram +
### Target event -A **name between parentheses** — for example, `(click)` — -identifies the target event. In the following example, the target is the button's click event. +As above, the target is the button's click event. - + -Some people prefer the `on-` prefix alternative, known as the **canonical form**: +Alternatively, use the `on-` prefix, known as the canonical form: - + Element events may be the more common targets, but Angular looks first to see if the name matches an event property of a known directive, as it does in the following example: - + -
- -The `myClick` directive is further described in the section -on [aliasing input/output properties](guide/template-syntax#aliasing-io). - -
- If the name fails to match an element event or an output property of a known directive, Angular reports an “unknown directive” error. + ### *$event* and event handling statements In an event binding, Angular sets up an event handler for the target event. @@ -1003,72 +990,73 @@ The template statement typically involves a receiver, which performs an action in response to the event, such as storing a value from the HTML control into a model. -The binding conveys information about the event, including data values, through -an **event object named `$event`**. +The binding conveys information about the event. This information can include data values such as an event object, string, or number named `$event`. -The shape of the event object is determined by the target event. +The target event determines the shape of the `$event` object. If the target event is a native DOM element event, then `$event` is a [DOM event object](https://developer.mozilla.org/en-US/docs/Web/Events), with properties such as `target` and `target.value`. Consider this example: - + -This code sets the input box `value` property by binding to the `name` property. -To listen for changes to the value, the code binds to the input box's `input` event. +This code sets the `` `value` property by binding to the `name` property. +To listen for changes to the value, the code binds to the `input` +event of the `` element. When the user makes changes, the `input` event is raised, and the binding executes the statement within a context that includes the DOM event object, `$event`. To update the `name` property, the changed text is retrieved by following the path `$event.target.value`. -If the event belongs to a directive (recall that components are directives), -`$event` has whatever shape the directive decides to produce. +If the event belongs to a directive—recall that components +are directives—`$event` has whatever shape the directive produces. -{@a eventemitter} -{@a custom-event} - -### Custom events with EventEmitter +### Custom events with `EventEmitter` Directives typically raise custom events with an Angular [EventEmitter](api/core/EventEmitter). The directive creates an `EventEmitter` and exposes it as a property. The directive calls `EventEmitter.emit(payload)` to fire an event, passing in a message payload, which can be anything. Parent directives listen for the event by binding to this property and accessing the payload through the `$event` object. -Consider a `HeroDetailComponent` that presents hero information and responds to user actions. -Although the `HeroDetailComponent` has a delete button it doesn't know how to delete the hero itself. -The best it can do is raise an event reporting the user's delete request. +Consider an `ItemDetailComponent` that presents item information and responds to user actions. +Although the `ItemDetailComponent` has a delete button, it doesn't know how to delete the hero. It can only raise an event reporting the user's delete request. -Here are the pertinent excerpts from that `HeroDetailComponent`: +Here are the pertinent excerpts from that `ItemDetailComponent`: - + + - + + The component defines a `deleteRequest` property that returns an `EventEmitter`. When the user clicks *delete*, the component invokes the `delete()` method, -telling the `EventEmitter` to emit a `Hero` object. +telling the `EventEmitter` to emit an `Item` object. -Now imagine a hosting parent component that binds to the `HeroDetailComponent`'s `deleteRequest` event. +Now imagine a hosting parent component that binds to the `deleteRequest` event +of the `ItemDetailComponent`. - + -When the `deleteRequest` event fires, Angular calls the parent component's `deleteHero` method, -passing the *hero-to-delete* (emitted by `HeroDetail`) in the `$event` variable. +When the `deleteRequest` event fires, Angular calls the parent component's +`deleteItem()` method, passing the *item-to-delete* (emitted by `ItemDetail`) +in the `$event` variable. ### Template statements have side effects -The `deleteHero` method has a side effect: it deletes a hero. -Template statement side effects are not just OK, but expected. +Though [template expressions](guide/template-syntax#template-expressions) shouldn't have [side effects](guide/template-syntax#avoid-side-effects), template +statements usually do. The `deleteItem()` method does have +a side effect: it deletes an item. -Deleting the hero updates the model, perhaps triggering other changes -including queries and saves to a remote server. -These changes percolate through the system and are ultimately displayed in this and other views. +Deleting an item updates the model, and depending on your code, triggers +other changes including queries and saving to a remote server. +These changes propagate through the system and ultimately display in this and other views.
diff --git a/aio/content/images/guide/event-binding/syntax-diagram.svg b/aio/content/images/guide/event-binding/syntax-diagram.svg new file mode 100644 index 0000000000..03a1fbec6d --- /dev/null +++ b/aio/content/images/guide/event-binding/syntax-diagram.svg @@ -0,0 +1 @@ +<button(click)="onSave()">Save</button>target event nametemplate statement \ No newline at end of file