Page 64 - MSDN Magazine, July 2017
P. 64
to verify that the Upvote class behaves the way it’s supposed to, as shown in Figure 1.
There’s obviously more you can do to test this (Can an Upvote have negative values? Can an Upvote be initialized with non-integer values?), but this covers the basics.
By the way, if you’re not already, you can run the Angular test- runner by (again) using the CLI to kick it off in a long-running process by running “ng test.” This will fire up a browser window, execute the tests and provide interactive feedback by running the tests every time the source code files change. In many respects, it’s even better than a compiler, so long as you’re good about writing tests.
Now that you have a model to work with, let’s work on the UpvoteComponent itself.
UpvoteComponent Use
It often helps to start from the perspective of how you want to use the component, so let’s take a moment and rewrite the AppCom- ponent’s view to think about how we’ll use the UpvoteComponent:
<upvote votes="10" (onIncrement)="upvoted($event)">Current upvotes:</upvote>
Youhavealittlebitofeverythinghere:somecontentbetweenthe tags you want to use as part of the UpvoteComponent rendering, an attribute (votes) to initialize the UpvoteComponent internal state and an event that you want to expose to the caller/user of the UpvoteComponent so that they can bind in a local (to the calling component) function that will receive an update every time the user clicks on the UpvoteComponent up-arrow.
It’s easiest first to provide the votes attribute syntax; this simply requires that you provide a field in the UpvoteComponent and decorate it using the @Input decorator, so that Angular can dis- cover it and wire up the necessary code to pull the value of votes and store it in that field. However, you’re going to do something simultaneously tricky and cool with TypeScript along the way, as shown in Figure 2.
Notice that “model” is declared as a private field that uses the Type- Script “intersection type” syntax; this means that the field model can be either a number or an Upvote object. You then test to see if it’s an Upvote. If it isn’t, then you test whether it’s a number, so that regard- less of what was passed in, you ultimately end up with an Upvote object holding the data in question. The truly mind-bending part of this code is in the “else” block—we assign “votes” a new Upvote object, initialized with votes. Because TypeScript is doing some deep code analysis on this block, it knows—thanks to the earlier
Figure 2 Initializing UpvoteComponent Using TypeScript “Intersection Type” Syntax
“if ” test that determines that votes is not an Upvote type—that it must be a number and, therefore, “this.votes” (temporarily) satisfies the condition that the Upvote constructor requires a number.
Next, notice how we would like to provide a label to the content of the UpvoteComponent, so that it can display the text along with a number (and an up-arrow). To do that, Angular lets you project content using the ng-content tag, essentially picking up the con- tent inside the app-upvote tag and dropping it somewhere in the view (defined, remember, in upvote.component.html), like so:
<p><ng-content></ng-content> {{votes.count}} ▲</p>
The Unicode character at the end is the Unicode code point for the up-arrow glyph, and recall that the double-curly-bracket syntax is string interpolation of the enclosed expression. The ng-content tag will then be replaced by whatever the user of the UpvoteComponent specified in the body of the tag, which in this case will be the text Current upvotes.
The truly mind-bending part of this code is in the “else” block— we assign “votes” a new Upvote object, initialized with votes.
Last, you want to provide a “push” sort of notification system, so that when the user clicks on the up-arrow, the count will increment and then notify interested parties. This is going to require three steps: knowing when the user clicks on the up-arrow (which will require trapping the event in the UpvoteComponent’s view), doing the Upvote increment and then pushing that out to the world.
In the first step, you need to trap when the user clicks on the up-arrow, so in the UpvoteComponent view, surround the up-arrow character in a span tag and attach a local method, clicked, to the click event using the Angular event-binding syntax, like so:
<p><!-- as before --> <span (click)="clicked()">▲</span></p>
This will call a method, clicked, that needs to be defined on the UpvoteComponent class, like so:
clicked() {
(this.votes as Upvote).increment(); this.onIncrement.emit((this.votes as Upvote).count);
}
Notice that because the “votes” field is an intersection type of Upvote or number, you need to explicitly cast it as an Upvote object in order to call the Upvote object’s increment method.
The third part is already wired in the clicked method—you use an EventEmitter object to send out a message to any interested parties. You pass the current vote count as the sole event param- eter to any parties that subscribe to this event. The EventEmitter is also declared as a field of the UpvoteComponent, but marked with the @Output decorator, like so:
export class UpvoteComponent implements OnInit {
@Output() onIncrement = new EventEmitter<number>();
// ... as before }
export class UpvoteComponent implements OnInit { @Input() votes : number | Upvote;
ngOnInit() {
if (this.votes instanceof Upvote) {
// It's already an Upvote }
else if (this.votes == undefined) { this.votes = new Upvote(0);
} else {
this.votes = new Upvote(this.votes); }
} }
58 msdn magazine
The Working Programmer