Skip to content

Commit ad1945b

Browse files
Dotneteerwardbell
authored andcommitted
Scenario #5 completed
1 parent 868073e commit ad1945b

File tree

10 files changed

+162
-10
lines changed

10 files changed

+162
-10
lines changed

public/docs/_examples/component-communication/ts/invite-heroes/src/app/invited-hero.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
// #docregion import
12
import {Component, Input} from 'angular2/angular2';
3+
// #enddocregion import
24
import {Hero} from './hero';
35

46
@Component({
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<!-- #docregion -->
2+
<invited-hero *ng-for='#hero of invitedHeroes'
3+
[hero]='hero' [request]='jobRequest'
4+
[job-board]='getJobBoard()'>
5+
</invited-hero>
6+
<!-- #enddocregion -->
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<!-- #docregion -->
2+
<button [disabled]="!request || undertaken"
3+
(click)='undertakeJob()'>
4+
I'll take it!
5+
</button>
6+
<!-- #enddocregion -->

public/docs/_examples/component-communication/ts/take-job-antipattern/src/app/hero-job-board.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,13 @@ export class HeroJobBoard{
100100
this.jobRequest = request.trim();
101101
}
102102

103-
getJobBoard() {
104-
return this;
105-
}
106-
103+
// #docregion get-job-board
107104
heroTakesJob(hero: Hero) {
108105
this.respondingHeroes.push(hero);
109106
}
107+
108+
getJobBoard() {
109+
return this;
110+
}
111+
// #enddocregion get-job-board
110112
}

public/docs/_examples/component-communication/ts/take-job-antipattern/src/app/invited-hero.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {HeroJobBoard} from './hero-job-board';
4646
}
4747
`]
4848
})
49+
// #docregion component
4950
export class InvitedHero {
5051
@Input() hero: Hero;
5152
@Input() request: string;
@@ -61,4 +62,5 @@ export class InvitedHero {
6162
this.jobBoard.heroTakesJob(this.hero);
6263
this.undertaken = true;
6364
}
64-
}
65+
}
66+
// #enddocregion component
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<!-- #docregion -->
2+
<invited-hero *ng-for='#hero of invitedHeroes'
3+
[hero]='hero' [request]='jobRequest'
4+
(on-hero-response)='heroTakesJob($event)'>
5+
</invited-hero>
6+
<!-- #enddocregion -->

public/docs/_examples/component-communication/ts/take-job-event/src/app/hero-job-board.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,9 @@ export class HeroJobBoard{
9999
announceJob(request) {
100100
this.jobRequest = request.trim();
101101
}
102-
102+
// #docregion hero-takes-job
103103
heroTakesJob(hero: Hero) {
104104
this.respondingHeroes.push(hero);
105105
}
106+
// #enddocregion hero-takes-job
106107
}

public/docs/_examples/component-communication/ts/take-job-event/src/app/invited-hero.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
// #docregion import
12
import {Component, Input, Output, EventEmitter} from 'angular2/angular2';
3+
// #enddocregion import
24
import {Hero} from './hero';
35
import {HeroJobBoard} from './hero-job-board';
46

@@ -46,6 +48,7 @@ import {HeroJobBoard} from './hero-job-board';
4648
}
4749
`]
4850
})
51+
// #docregion component
4952
export class InvitedHero {
5053
@Input() hero: Hero;
5154
@Input() request: string;
@@ -62,4 +65,5 @@ export class InvitedHero {
6265
this.onHeroResponse.next(this.hero);
6366
this.undertaken = true;
6467
}
65-
}
68+
}
69+
// #enddocregion component

public/docs/ts/latest/guide/component-communication.jade

Lines changed: 126 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,11 @@ figure.image-display
177177

178178
+makeExample('component-communication/ts/invite-heroes/src/app/invited-hero.ts', 'input-binding', 'invited-hero.ts', {pnk: /(@Input\(\))/g})
179179

180+
:marked
181+
`Input` is defined in the core module of Angular, and we need to import it:
182+
183+
+makeExample('component-communication/ts/invite-heroes/src/app/invited-hero.ts', 'import', null, {blk: /(Input)/g})
184+
180185
:marked
181186
Within the template definition of `InvitedHero`, we can use the `hero` property to display data, ie. the name of the hero:
182187

@@ -303,15 +308,133 @@ figure.image-display
303308

304309
:marked
305310
## #4: A hero takes the job &mdash; child to parent communication &mdash; antipattern
306-
_Content_
311+
312+
Let's make a step further. In this scenario, we implement the functionality of the **I'll take it!** button &mdash show how a child can communicate
313+
with its parent. What if the parent were expose a function &mdash; let's call it `heroTakesJob()` &mdash; and an `InvitedHero` instance called it? It sounds viable,
314+
but the child component needs to have a reference to its parent. Well, with data binding, the parent can hand a reference on itself to its children, can't it?
315+
The temptation of doing child-to-parent communication this way is great. In this scenario, we demonstrate how easy it is.
316+
317+
.alert.is-critical
318+
:marked
319+
** What we are going to do, is a bad practice to avioid &mdash; it is an antipattern.** We still demonstrate it, and then explain why it is bad.
320+
:marked
321+
Let's append two methods to `HeroJobBoard`, `heroTakesJob` and `getJobBoard`, respectively. As the names suggest, the first carries out
322+
the administration of taking a heros response, the second retrieves the `HeroJobBoard` instance:
323+
324+
+makeExample('component-communication/ts/take-job-antipattern/src/app/hero-job-board.ts', 'get-job-board', 'hero-job-board.ts')
325+
326+
:marked
327+
With a slight modification in the `HeroJobBoard` template, the parent can hand itself to its children, provided `InvitedHero` has an input property,
328+
`jobBoard`, which stores this reference:
329+
330+
+makeExample('component-communication/ts/take-job-antipattern/fragments/invited-hero.html', null, 'hero-job-board.ts (extract from template)', {blk: /(job-board)/g})
331+
332+
:marked
333+
The `InvitedHero` component can obtain this reference as soon as we specify its `jobBoard` property. We add the `undertakeJob()` method, too, and implement
334+
it so that it invokes the parent's' `heroTakesJob()` method through `this.jobBoard`.
335+
336+
+makeExample('component-communication/ts/take-job-antipattern/src/app/invited-hero.ts', 'component', 'invited-hero.ts', {blk: /(jobBoard|undertakeJob\(\)|this\.jobBoard)/g})
307337

338+
:marked
339+
To complete this scenario, we modify the `InvitedHero` template's **I'll take it!** to call `undertakeJob()`:
340+
341+
+makeExample('component-communication/ts/take-job-antipattern/fragments/undertake-job.html', null, 'invited-hero.ts (extract from template)')
342+
343+
:marked
344+
When running the app, heroes can be invited, they listen to job requests, and are able to undertake a job, as this UI snapshot shows:
345+
346+
figure.image-display
347+
img(src="/resources/images/devguide/component-communication/take-job-ui.png" alt="Heroes undertake job")
348+
349+
:marked
350+
### Why this practice is bad
351+
352+
Distributing the application's set of responsibilities among components that have clean boundaries is a great way to ensure that our app
353+
is easier to implement, maintain, test, and fix. In this chapter, instead of having a monolithic application component, we created three components, `HeroJobApp`, `HeroJobBoard`,
354+
and `InvitedHero`. Each component has well-defined responsibilities. `InvitedHero` does two things: first, it can receive and display job request notifications,
355+
second, it can notify int context &mdash; its parent &mdash; about the fact that a hero undertakes the job.
356+
357+
In this scenario, we used tightly-coupled binding between an `InvitedHero` instance and its parent, `HeroJobBoard`. This design hurts the autonomy
358+
of both component types, and does not help a real separation of responsibilities. Instead of simply notifying `HeroJobBoard` and trust in that it can administer
359+
the list of responding heroes, `InvitedHero` takes over this task, and invokes the `heroTakesJob()` method directly.
360+
361+
Eventually, this pattern makes our application fragile &mdash; so we take it into account as an _antipattern_.
362+
363+
By passing a reference to itself, `HeroJobBoard` offers its entire functionality to an `InvitedHero` instance. This way nothing can prevent `InvitedHero` to use
364+
other `HeroJobBoard` operations, for example, it could invoke even `announceJob()`, and that hurts our workflow design as badly as the autonomity of components. Giving up
365+
the fundamental desing principles based on properly used and clean component decomposition prevents us from creating easily maintainable and testable software,
366+
and creates technical debt.
367+
368+
.alert.is-important
369+
:marked
370+
The antipattern we used in this scenario couples only two component types. In real applications, there are often deeper component trees with longer chains of
371+
parents, children, grandchildren, and so on. This issues coming from this kind of tight coupling among components definitely worsen the situation, and causes more
372+
maintainance and testing headaches &mdash; while increasing the technical debt, often exponentially.
373+
:marked
308374
## #5: A hero takes the job &mdash; child to parent communication with events
309375

310-
_Content _
376+
Knowing that the the tight coupling we used in the previous scenario is an antipattern, it's time to learn how we can avoid such a situation with Angular.
377+
378+
The task we want to solve correctly is that `InvitedHero` needs to notify its parent `HeroJobBoard` that a hero undertaks the requested job so that the hero can be
379+
added to the list of responding ones. We will use _event binding_ to communicate from a children to its parent. We create an _output property_ on `InvitedHero`,
380+
and in the template of `HeroJobBoard` &mdash; where `InvitedHero` is specified with the `<invited-hero>` element &mdash; we bind the output property to a
381+
`HeroJobBoard` event handler method.
382+
383+
We can easily add an output property to `InvitedHero`. We name this new property `onHeroResponse`, and use it within the `undertakeJob()` method:
384+
385+
+makeExample('component-communication/ts/take-job-event/src/app/invited-hero.ts', 'component', 'invited-hero.ts', {blk: /(onHeroResponse|undertakeJob\(\))/g})
386+
387+
:marked
388+
We annotated the output parameter with the `@Output()` decorator, and initialized it to a new `EventEmitter<Hero>` instance.
389+
390+
.alert.is-important
391+
:marked
392+
Again, do not forget to use the parentheses with `@Output()` so that our application would not fail mysteriously.
393+
:marked
394+
In order to use these new types, we must import them from the core Angular module:
395+
396+
+makeExample('component-communication/ts/take-job-event/src/app/invited-hero.ts', 'import', null, {blk: /(Output|EventEmitter)/g})
397+
398+
:marked
399+
Being an output parameter means that an external component can bind an event handler method in its template that responds the event raised with the object
400+
behind the output parameter. As the body of `undertakJob()` shows, we can notify the event that a hero undertook the job through calling
401+
`this.onHeroResponse.next(this.hero)`. The `onHeroResponse` property's type is `EventEmitter<Hero>`. As its name suggest, an instance of this type can emit events
402+
that are represented with arguments of type `Hero`. In our very case, with the event we pass the `Hero` instance that undertook the job.
403+
404+
.alert.is-helpful
405+
:marked
406+
The `EventEmitter` generic type is built on the features of the `Observable` type of ECMAScript standard library. `Observable` is a type that
407+
can be used to model push-based data sources such as DOM events, timer intervals, and sockets. Understanding this type requires a detailed discussion,
408+
and in this chapter we are not going to leverage the features of observables.
409+
410+
`EventEmitter` can be used not only for output parameters, but in other publish-subscribe scenarios, as we will learn it soon.
411+
:marked
412+
`HeroJobBoard` can bind itself to the `onHeroResponse` event:
413+
414+
+makeExample('component-communication/ts/take-job-event/fragments/invited-hero.html', null, 'hero-job-board.ts (extract from template)', {blk: /(\(on-hero-response\))/g})
415+
416+
:marked
417+
The event is bound to the `heroTakesJob()` method, and `$event` represents the `Hero` instance passed by `InvitedHero` as the event argument, which is the hero who
418+
undertook the job. The `heroTakesJob()` method is exactly the same as we used in the previous scenario:
419+
420+
+makeExample('component-communication/ts/take-job-event/src/app/hero-job-board.ts', 'hero-takes-job', 'hero-job-board.ts')
421+
422+
:marked
423+
Now, with these simple steps, our app works just like at the end of the previous scenario &mdash; but this time it uses a supported child-to-parent communication pattern.
424+
425+
### Why this practice is preferred
426+
427+
In this scenario, out components kept their autonomy, in constrast to the antipattern treated in the previous scenario. `InvitedHero` gets the job request through an
428+
input property, but actually does not know from which component the job request is bound to its `request` property. Similarly, when a hero undertakes the job,
429+
`InvitedHero` emits an event through its `onHeroResponde` output property without knowing which component (or components) listen to this event.
430+
431+
This architecture allows easy maintenance and testing, as our components encapsulate everything they are responsible for. They do not have strong dependencies on
432+
other components. When we face with child-to-parent communication, using events as we did in this scenario is a _viable way to avoid the traps and fragility_ of the
433+
antipattern demonstrated in the previous section.
311434

312435
## #6: Refactoring communication to an intermediary service
313436

314-
_Content_
437+
There is still a tiny flaw in our inter-component communication design. The `HeroJobBoard` and `InvitedHero` components >>> _to be continued_
315438

316439
## #7: Assigning the job to a hero &mdash; extending the intermediary service
317440

Loading

0 commit comments

Comments
 (0)