include ../_util-fns :marked This cookbook contains recipes for common component communication scenarios in which two or more components share information. 本烹饪宝典包含了常见的组件通讯场景,也就是让两个或多个组件之间共享信息的方法。 .l-sub-section :marked For an in-depth look at each fundamental concepts in component communication, we can find detailed description and samples in the [Component Communication]() document. 要深入了解组件通讯的各个基本概念,我们可以在[组件通讯Component Communication]()文档中可以找到详细的描述和例子。 :marked ## Table of contents ## 目录 [Pass data from parent to child with input binding](#parent-to-child) [使用输入型绑定,把数据从父组件传到子组件](#parent-to-child) [Intercept input property changes with a setter](#parent-to-child-setter) [通过setter拦截输入属性值的变化](#parent-to-child-setter) [Intercept input property changes with *ngOnChanges*](#parent-to-child-on-changes) [使用*ngOnChanges*拦截输入属性值的变化](#parent-to-child-on-changes) [Parent listens for child event](#child-to-parent) [父组件监听子组件的事件](#child-to-parent) [Parent interacts with child via a *local variable*](#parent-to-child-local-var) [父组件与子组件通过*本地变量local variable*互动](#parent-to-child-local-var) [Parent calls a *ViewChild*](#parent-to-view-child) [父组件调用*ViewChild*](#parent-to-view-child) [Parent and children communicate via a service](#bidirectional-service) [父组件和子组件通过服务来通讯](#bidirectional-service) :marked **See the [live example](/resources/live-examples/cb-component-communication/ts/plnkr.html)**. **参见[在线例子](/resources/live-examples/cb-component-communication/ts/plnkr.html)** .l-main-section :marked ## Pass data from parent to child with input binding ## 通过输入型绑定把数据从父组件传到子组件。 `HeroChildComponent` has two ***input properties***, typically adorned with [@Input decorations](../guide/template-syntax.html#inputs-outputs). `HeroChildComponent` 有两个***输入型属性***,它们通常带[@Input装饰器](../guide/template-syntax.html#inputs-outputs)。 +makeExample('cb-component-communication/ts/app/hero-child.component.ts') :marked The second `@Input` aliases the child component property name `masterName` as `'master'`. 第二个`@Input`为子组件的属性名`masterName`指定一个别名`master`(译者注:不推荐为起别名,请参见风格指南). The `HeroParentComponent` nests the child `HeroChildComponent` inside an `*ngFor` repeater, binding its `master` string property to the child's `master` alias and each iteration's `hero` instance to the child's `hero` property. 父组件`HeroParentComponent`把子组件的`HeroChildComponent`放到`*ngFor`循环器中,把自己的`master`字符串属性绑定到子组件的`master`别名上,并把每个循环的`hero`实例绑定到子组件的`hero`属性。 +makeExample('cb-component-communication/ts/app/hero-parent.component.ts') :marked The running application displays three heroes: 运行应用程序会显示三个英雄: figure.image-display img(src="/resources/images/cookbooks/component-communication/parent-to-child.png" alt="Parent-to-child") :marked ### Test it ### 测试 E2E test that all children were instantiated and displayed as expected: 端到端测试,用于确保所有的子组件都像我们所期待的那样被初始化并显示出来。 +makeExample('cb-component-communication/e2e-spec.js', 'parent-to-child') :marked [Back to top](#top) [回到顶部](#top) .l-main-section :marked ## Intercept input property changes with a setter ## 通过setter截听输入属性值的变化 Use an input property setter to intercept and act upon a value from the parent. 使用一个输入属性的setter,以拦截父组件中值的变化,并采取行动。 The setter of the `name` input property in the child `NameChildComponent` trims the whitespace from a name and replaces an empty value with default text. 子组件`NameChildComponent`的输入属性`name`上的这个setter,会trim掉名字里的空格,并把空值替换成默认字符串。 +makeExample('cb-component-communication/ts/app/name-child.component.ts') :marked Here's the `NameParentComponent` demonstrating name variations including a name with all spaces: 下面的`NameParentComponent`展示了各种名字的处理方式,包括一个全是空格的名字。 +makeExample('cb-component-communication/ts/app/name-parent.component.ts') figure.image-display img(src="/resources/images/cookbooks/component-communication/setter.png" alt="Parent-to-child-setter") :marked ### Test it ### 测试 E2E tests of input property setter with empty and non-empty names: 端到端测试:输入属性的setter,分别使用空名字和非空名字。 +makeExample('cb-component-communication/e2e-spec.js', 'parent-to-child-setter') :marked [Back to top](#top) [回到顶部](#top) .l-main-section :marked ## Intercept input property changes with *ngOnChanges* ## 通过*ngOnChanges*来截听输入属性值的变化 Detect and act upon changes to input property values with the `ngOnChanges` method of the `OnChanges` lifecycle hook interface. 使用`OnChanges`生命周期钩子接口的`ngOnChanges`方法来监测输入属性值的变化并做出回应。 .l-sub-section :marked May prefer this approach to the property setter when watching multiple, interacting input properties. 当需要监视多个、交互式输入属性的时候,本方法比用属性的setter更合适。 Learn about `ngOnChanges` in the [LifeCycle Hooks](../guide/lifecycle-hooks.html) chapter. 学习关于`ngOnChanges`的更多知识,参见[生命周期钩子](../guide/lifecycle-hooks.html)一章。 :marked This `VersionChildComponent` detects changes to the `major` and `minor` input properties and composes a log message reporting these changes: 这个`VersionChildComponent`会监测输入属性`major`和`minor`的变化,并把这些变化编写成日志以报告这些变化。 +makeExample('cb-component-communication/ts/app/version-child.component.ts') :marked The `VersionParentComponent` supplies the `minor` and `major` values and binds buttons to methods that change them. `VersionParentComponent`提供`minor`和`major`值,把修改它们值的方法绑定到按钮上。 +makeExample('cb-component-communication/ts/app/version-parent.component.ts') :marked Here's the output of a button-pushing sequence: 下面是点击按钮的结果。 figure.image-display img(src="/resources/images/cookbooks/component-communication/parent-to-child-on-changes.gif" alt="Parent-to-child-onchanges") :marked ### Test it ### 测试 Test that ***both*** input properties are set initially and that button clicks trigger the expected `ngOnChanges` calls and values: 测试确保***这两个***输入属性值都被初始化了,当点击按钮后,`ngOnChanges`应该被调用,属性的值也符合预期。 +makeExample('cb-component-communication/e2e-spec.js', 'parent-to-child-onchanges') :marked [Back to top](#top) [回到顶部](#top) .l-main-section :marked ## Parent listens for child event ## 父组件监听子组件的事件 The child component exposes an `EventEmitter` property with which it `emits`events when something happens. The parent binds to that event property and reacts to those events. 子组件暴露一个`EventEmitter`属性,当事件发生时,子组件利用该属性`emits`(向上弹射)事件。父组件绑定到这个事件属性,并在事件发生时作出回应。 The child's `EventEmitter` property is an ***output property***, typically adorned with an [@Output decoration](../guide/template-syntax.html#inputs-outputs) as seen in this `VoterComponent`: 子组件的`EventEmitter`属性是一个**输出属性**,通常带有[@Output装饰器](../guide/template-syntax.html#inputs-outputs),就像在`VoterComponent`中看到的。 +makeExample('cb-component-communication/ts/app/voter.component.ts') :marked Clicking a button triggers emission of a `true` or `false` (the boolean *payload*). 点击按钮会触发`true`或`false`(布尔型*有效载荷*)的事件。 The parent `VoteTakerComponent` binds an event handler (`onVoted`) that responds to the child event payload (`$event`) and updates a counter. 父组件`VoteTakerComponent`绑定了一个事件处理器(`onVoted`),用来响应子组件的事件(`$event`)并更新一个计数器。 +makeExample('cb-component-communication/ts/app/votetaker.component.ts') :marked The framework passes the event argument — represented by `$event` — to the handler method, and the method processes it: 框架(Angular)把事件参数(用`$event`表示)传给事件处理方法,这个方法会处理: figure.image-display img(src="/resources/images/cookbooks/component-communication/child-to-parent.gif" alt="Child-to-parent") :marked ### Test it ### 测试 Test that clicking the *Agree* and *Disagree* buttons update the appropriate counters: 测试确保点击*Agree*和*Disagree*按钮时,计数器被正确更新。 +makeExample('cb-component-communication/e2e-spec.js', 'child-to-parent') :marked [Back to top](#top) [回到顶部](#top) parent-to-child-local-var .l-main-section :marked ## Parent interacts with child via *local variable* ## 父组件与子组件通过*本地变量*互动 A parent component cannot use data binding to read child properties or invoke child methods. We can do both by creating a template reference variable for the child element and then reference that variable *within the parent template* as seen in the following example. 父组件不能使用数据绑定来读取子组件的属性或调用子组件的方法。但我们可以在父组件模板里,新建一个本地变量来代表子组件,然后利用这个变量来读取子组件的属性和调用子组件的方法,如下例所示。 We have a child `CountdownTimerComponent` that repeatedly counts down to zero and launches a rocket. It has `start` and `stop` methods that control the clock and it displays a countdown status message in its own template. 子组件`CountdownTimerComponent`进行倒计时,归零时发射一个导弹。`start`和`stop`方法负责控制时钟并在模板里显示倒计时的状态信息。 +makeExample('cb-component-communication/ts/app/countdown-timer.component.ts') :marked Let's see the `CountdownLocalVarParentComponent` that hosts the timer component. 让我们来看看计时器组件的宿主组件`CountdownLocalVarParentComponent`。 +makeExample('cb-component-communication/ts/app/countdown-parent.component.ts', 'lv') :marked The parent component cannot data bind to the child's `start` and `stop` methods nor to its `seconds` property. 父组件不能通过数据绑定使用子组件的`start`和`stop`方法,也不能访问子组件的`seconds`属性。 We can place a local variable (`#timer`) on the tag (``) representing the child component. That gives us a reference to the child component itself and the ability to access *any of its properties or methods* from within the parent template. 我们把本地变量(`#timer`)放到(``)标签中,用来代表子组件。这样父组件的模板就得到了子组件的引用,于是可以在父组件的模板中访问子组件的所有属性和方法。 In this example, we wire parent buttons to the child's `start` and `stop` and use interpolation to display the child's `seconds` property. 在这个例子中,我们把父组件的按钮绑定到子组件的`start`和`stop`方法,并用插值表达式来显示子组件的`seconds`属性。 Here we see the parent and child working together. 下面是父组件和子组件一起工作时的效果。 figure.image-display img(src="/resources/images/cookbooks/component-communication/countdown-timer-anim.gif" alt="countdown timer") a(id="countdown-tests") :marked ### Test it ### 测试 Test that the seconds displayed in the parent template match the seconds displayed in the child's status message. Test also that clicking the *Stop* button pauses the countdown timer: 测试确保在父组件模板中显示的秒数和子组件状态信息里的秒数同步。它还会点击*Stop*按钮来停止倒计时: +makeExample('cb-component-communication/e2e-spec.js', 'countdown-timer-tests') :marked [Back to top](#top) [回到顶部](#top) .l-main-section :marked ## Parent calls a *ViewChild* ## 父组件调用*ViewChild* The *local variable* approach is simple and easy. But it is limited because the parent-child wiring must be done entirely within the parent template. The parent component *itself* has no access to the child. 这个*本地变量*方法是个简单便利的方法。但是它也有局限性,因为父组件-子组件的连接必须全部在父组件的模板中进行。父组件本身的代码对子组件没有访问权。 We can't use the *local variable* technique if an instance of the parent component *class* must read or write child component values or must call child component methods. 如果父组件的*类*需要读取子组件的属性值或调用子组件的方法,我们就不能使用*本地变量*方法了。 When the parent component *class* requires that kind of access, we ***inject*** the child component into the parent as a *ViewChild*. 当父组件*类*需要这种访问时,我们可以把子组件作为*ViewChild*,***注入***到父组件里面。 We'll illustrate this technique with the same [Countdown Timer](#countdown-timer-example) example. We won't change its appearance or behavior. The child [CountdownTimerComponent](#countdown-timer-example) is the same as well. 我们将会用同一个[倒计时](#countdown-timer-example)范例来解释这种技术。我们没有改变它的样子或行为。子组件[CountdownTimerComponent](#countdown-timer-example)也和原来一样。 .l-sub-section :marked We are switching from the *local variable* to the *ViewChild* technique solely for the purpose of demonstration. 我们由*本地变量*切换到*ViewChild*技术的唯一目的就是做示范。 :marked Here is the parent, `CountdownViewChildParentComponent`: 下面是父组件`CountdownViewChildParentComponent`: +makeExample('cb-component-communication/ts/app/countdown-parent.component.ts', 'vc') :marked It takes a bit more work to get the child view into the parent component classs. 把子组件的视图插入到父组件类需要做一点额外的工作。 We import references to the `ViewChild` decorator and the `AfterViewInit` lifecycle hook. 我们需要通过`ViewChild`装饰器导入这个引用,并挂上`AfterViewInit`生命周期钩子。 We inject the child `CountdownTimerComponent` into the private `timerComponent` property via the `@ViewChild` property decoration. 我们通过`@ViewChild`属性装饰器,将子组件`CountdownTimerComponent`注入到私有属性`timerComponent`里面。 The `#timer` local variable is gone from the component metadata. Instead we bind the buttons to the parent component's own `start` and `stop` methods and present the ticking seconds in an interpolation around the parent component's `seconds` method. 组件元数据里就不再需要`#timer`本地变量了。而是把按钮绑定到父组件自己的`start`和`stop`方法,使用父组件的`seconds`方法的插值表达式来展示秒数变化。 These methods access the injected timer component directly. 这些方法可以直接访问被注入的计时器组件。 The `ngAfterViewInit` lifecycle hook is an important wrinkle. The timer component isn't available until *after* Angular displays the parent view. So we display `0` seconds initially. `ngAfterViewInit`生命周期钩子是非常重要的一步。被注入的计时器组件只有在Angular显示了父组件视图之后才能访问,所以我们先把秒数显示为0. Then Angular calls the `ngAfterViewInit` lifecycle hook at which time it is *too late* to update the parent view's display of the countdown seconds. Angular's unidirectional data flow rule prevents us from updating the parent view's in the same cycle. We have to *wait one turn* before we can display the seconds. 然后Angular会调用`ngAfterViewInit`生命周期钩子,但这时候再更新父组件视图的倒计时就已经太晚了。Angular的单向数据流规则会阻止我们在同一个周期内更新父组件视图。我们在显示秒数之前会被迫*再等一轮儿*。 We use `setTimeout` to wait one tick and then revise the `seconds` method so that it takes future values from the timer component. 我们使用`setTimeout`来等下一轮儿,然后改写`seconds`方法,这样它接下来就会从注入的这个计时器组件里获取秒数的值。 ### Test it ### 测试 Use [the same countdown timer tests](#countdown-tests) as before. 使用和之前[一样的倒计时测试](#countdown-tests)。 :marked [Back to top](#top) [回到顶部]('#top') .l-main-section :marked ## Parent and children communicate via a service ## 父组件和子组件通过服务来通讯 A parent component and its children share a service whose interface enables bi-directional communication *within the family*. 父组件和它的子组件共享同一个服务,利用该服务*在家庭内部*实现双向通讯。 The scope of the service instance is the parent component and its children. Components outside this component subtree have no access to the service or their communications. 该服务实例的作用范围被限制在父组件和其子组件内。这个组件子树之外的组件将无法访问该服务或者与它们通讯。 This `MissionService` connects the `MissionControlComponent` to multiple `AstronautComponent` children. 这个`MissionService`把`MissionControlComponent`和多个`AstronautComponent`子组件连接起来。 +makeExample('cb-component-communication/ts/app/mission.service.ts') :marked The `MissionControlComponent` both provides the instance of the service that it shares with its children (through the `providers` metadata array) and injects that instance into itself through its constructor: `MissionControlComponent`提供服务的实例,并将其共享给它的子组件(通过`providers`元数据数组),子组件可以通过构造函数将该实例注入到自身。 +makeExample('cb-component-communication/ts/app/missioncontrol.component.ts') :marked The `AstronautComponent` also injects the service in its constructor. Each `AstronautComponent` is a child of the `MissionControlComponent` and therefore receives its parent's service instance: `AstronautComponent`也通过自己的构造函数注入该服务。由于每个`AstronautComponent`都是`MissionControlComponent`的子组件,所以它们获取到的也是父组件的这个服务实例。 +makeExample('cb-component-communication/ts/app/astronaut.component.ts') .l-sub-section :marked Notice that we capture the `subscription` and unsubscribe when the `AstronautComponent` is destroyed. This is a memory-leak guard step. There is no actual risk in this app because the lifetime of a `AstronautComponent` is the same as the lifetime of the app itself. That *would not* always be true in a more complex application. 注意,我们通过`subscription`服务订阅任务,并在`AstronautComponent`被销毁的时候退订。这是一个用于防止内存泄漏的保护措施。实际上,在这个应用程序中并没有这个风险,因为`AstronautComponent`的生命期和应用程序的生命期一样长。但在更复杂的应用程序环境中就不一定了。 We do not add this guard to the `MissionControlComponent` because, as the parent, it controls the lifetime of the `MissionService`. 我们不需要在`MissionControlComponent`中添加这个保护措施,因为作为父组件,它控制着`MissionService`的生命期。 :marked The *History* log demonstrates that messages travel in both directions between the parent `MissionControlComponent` and the `AstronautComponent` children, facilitated by the service: *History*日志证明了:在父组件`MissionControlComponent`和子组件`AstronautComponent`之间,信息通过该服务实现了双向传递。 figure.image-display img(src="/resources/images/cookbooks/component-communication/bidirectional-service.gif" alt="bidirectional-service") :marked ### Test it ### 测试 Tests click buttons of both the parent `MissionControlComponent` and the `AstronautComponent` children and verify that the *History* meets expectations: 测试确保点击父组件`MissionControlComponent`和子组件`AstronautComponent`两个的组件的按钮时,*History*日志和预期的一样。 +makeExample('cb-component-communication/e2e-spec.js', 'bidirectional-service') :marked [Back to top](#top) [回到顶部](#top)