Angular 2 directive – square

This article is part of a series – the contents page is here.

In the last article we looked at the component that displays a piece.  It was pretty simple, not least because it didn’t support any kind of user input.  This time we’ll deal with the display of an individual board square.  We’ll make the square respond to mouse clicks so that the user can select it.

My first implementation of a board square was a component – a lot like the piece component.  However I eventually ran into an annoying quirk on the Edge browser that forced me to rewrite it as a directive, for reasons that I shall explain.

Consuming the directive

Let’s begin as we did with the piece component – by looking at how we will consume the square directive.  Here’s the relevant snippet from game.component.html:

<div [square]="boardSquare" *ngFor="let boardSquare of boardSquares" (selected)="onSquareSelected($event)"></div>

You might have been expecting to see a <square> element (just as we used a <square> element for the piece component).  Instead, we have a plain old <div> element with a [square] attribute.  If we ignore that difference for a moment we will find a couple of things that look similar to what we saw in the piece component:

  • There’s an *ngFor loop to emit a <div> for each of the board squares.
  • The boardSquare loop control variable’s value is being injected into the directive by the use of the square brackets around the [square] attribute.

There’s one more thing that’s new: the (selected) attribute.  Even if you haven’t used Angular you can probably guess that this is some sort of event binding.  The idea is that the square directive exposes an Output property called selected that fires an event when the square is selected by the user.  We’re handling that event using a method in our container (which is actually our game component) so that we can keep global track of the currently selected square.  The important point here is the round brackets around (selected).  That tells Angular that we’re binding to an Output property.  When you see round brackets around an attribute, think the component/directive is emitting information for the container to consume.

Declaring the directive

Here’s the code for the directive from square.component.ts (okay, I should probably rename that).  In other articles I’ve snipped bits out to keep the code fragments short, but this time I think it’s worthwhile to post the whole thing:

import { Directive, Input, Output, HostBinding, HostListener, OnInit, EventEmitter } from '@angular/core';
import * as Chess from '../engine/ChessElements';

@Directive({
    selector: '[square]',
})
export class SquareComponent implements OnInit {

    @Input("square") public square: Chess.BoardSquare;

    public file: number;
    public rank: number;
    public isLightColour: boolean;
    public algebraicName: string;
    public isSelected: boolean;

    @Output() selected: EventEmitter<SquareComponent> = new EventEmitter();

    constructor() {
        this.isSelected = false;
    }

    ngOnInit() {
        this.file = this.square.file;
        this.rank = this.square.rank;
        this.isLightColour = ((this.rank - 1) * 8 + this.file + (this.rank % 2)) % 2 === 1;
        this.algebraicName = this.square.algebraicNotation;
    }

    @HostBinding('style.-ms-grid-row') get rowBinder() {
        return 9 - this.rank;
    }

    @HostBinding('style.-ms-grid-column') get colBinder() {
        return this.file;
    }

    @HostBinding('id') get idBinder() {
        return this.algebraicName;
    }

    @HostBinding('class') get classBinder() {
        let classes = "boardsquare";
        classes += (this.isLightColour ? " lightsquare" : " darksquare");
        if (this.isSelected) {
            classes += " selectedFromSquare";
        }
        return classes;
    }

    @HostListener('click') onclick() {
        this.selected.emit(this);
    };

    public select(): void {
        this.isSelected = true;
    }

    public deselect(): void {
        this.isSelected = false;
    }
}

The list in the first import line at the top looks a bit more intimidating this time.  The only type that’s familiar from the piece component is Input.  The first one we need to discuss is Directive, which is the @Directive decorator.  The good news is that Directive is a lot like Component.  In fact, it’s pretty much a subset.  Notice that the only property we use in the @Directive decorator is selector.  We don’t use the template properties for the very good reason that they don’t exist.  We’re creating an attribute directive that changes the behaviour of an existing element, so there’s no new element and hence no template.

The directive selector

Let’s zoom in on the @Directive decorator and the @Input property declaration, because they’re important:

@Directive({
    selector: '[square]',
})
export class SquareComponent implements OnInit {

    @Input("square") public square: Chess.BoardSquare;
...
}

The selector for this directive is [square], which explains how the directive gets wired up to the <div> that we declared at the beginning.  Recall that the syntax was:

<div [square]="boardSquare" *ngFor="let boardSquare of boardSquares" (selected)="onSquareSelected($event)"></div>

…so the [square] input attribute seems to be doing two tasks.  It amplifies the <div> with our square directive and it pushes the boardSquare object into an Input property called … “square”?

Yep, that’s what’s happening.  On the one hand it gives us a rather slick syntax; on the other hand it’s a little confusing.  Is “[square]=” a reference to a directive or an input property?  Apparently, it’s both!  Notice that we declare @Input(“square”) at the top of the SquareComponent class.  It makes sense when you realise that triggering the creation of the directive and binding its input properties are two separate phases in Angular; a property used in one phase can have the same name as another property used in the other phase.

That puts us in a familiar position: we’ve got an instance of SquareComponent that has been given a BoardSquare object that tells it about the board square that it is responsible for rendering.

OnInit

We want to style the square with the right colour – light or dark (since we’re the UI it seems fair enough that we should be responsible for that; the engine doesn’t care about the difference between light and dark squares).  Seems like we need an isLightColour flag.  Since it involves a fiddly calculation, and since its value for a given square never changes, it would be nice to compute and cache it up front.  Couldn’t we do that in the constructor?  No!  We can’t do it until the [square] input has been initialized.  That’s why we have to hook up the Angular ‘init’ event.  We do this by (1) having our directive class implement the OnInit interface and (2) adding an ngOnInit method, where we can safely use this.square.

@HostBinding

Okay, so we know whether we’re a light or dark square.  But how can we manipulate the <div>‘s class attribute value?  Answer: with the @HostBinding decorator…

    @HostBinding('class') get classBinder() {
        let classes = "boardsquare";
        classes += (this.isLightColour ? " lightsquare" : " darksquare");
        if (this.isSelected) {
            classes += " selectedFromSquare";
        }
        return classes;
    }

There’s no other wiring required; notice that there’s no mention of the “class” attribute in the original <div> element.  Our directive has got its claws into that <div> and is now manipulating it like crazy!  Angular will call classBinder() regularly – notice how we can add the “selectedFromSquare” style when the square is in the selected state (this puts a green border around the square).  The only thing that’s a little confusing is the method name “classBinder”.  Is this some kind of magic name?  No!  In fact, you can use any name you like.  The only thing that matters to Angular is the literal string "class" inside @HostBinding.

We do a similar trick for the 'style.-ms-grid-row' and 'style.-ms-grid-column' attributes (these are used in the Edge browser to position the div in the grid).  I hope that you are starting to feel that this directive is not that different from a component.  The main difference is that we use @HostBinding instead of attributes in a template element to bind the data in our class to the DOM.

Handling selection events

The square directive has one more thing that the piece component didn’t.  Not because it’s a directive, but because it supports selection via mouse clicks.  (BTW: this is why the piece.component.css puts the pointer-events: none style onto each piece’s image – so that when you click on a piece image the click event will fall through to the square underneath).

The UI requirement is pretty simple.  When a human player wants to move, they start by clicking on the square containing the piece that they want to move, then they click on the square to which they want to move it.  Assuming that this represents a legal move, the second click will commit that move.  Of course, having selected the first square they might change their mind, so a second click on the first square will simply de-select that first square.

The first thing to realise is that the square directive is not in a position to drive all this logic, because it doesn’t know about the players, the other squares or the state of the game.  The only things it can do are (1) keep track of whether or not it is in the selected state; (2) highlight itself in the selected state; and (3) tell the world when it is clicked.

@Output, EventEmitter and @HostListener

In Angular 2 a component or directive can ‘tell the world’ about events through an @Output property of type EventEmmitter:

@Output() selected: EventEmitter<SquareComponent> = new EventEmitter();

This declares an event to which out container can subscribe.  Remember the original <div>?  Notice how it hooks up a handler to the selected event.  The event can send an object in its message.  In our case the directive object will simply send itself as the message – hence the reference to the SquareComponent type.

That explains how the directive’s container is wired up to receive the selection event.  Now we just have to fire the event whenever we get a mouse click.  That’s where @HostListener comes in:

@HostListener('click') onclick() { this.selected.emit(this); };

Like @HostBinding, @HostListener connects a method in our directive class to an aspect of the DOM element that ‘hosts’ us.  But whereas @HostBinding connects us to our div’s attributes, @HostListener connects us to our div’s events – in this case, the click event.  In response to the click event we call the emit() method of selected, passing ourselves as the event’s message body.

So why is it a directive?

We’ve covered a lot in this article but we still haven’t answered the question: why use a directive?  Wouldn’t it have been more natural to have a custom element with its own attributes instead of using @HostBinding and @HostListener to amplify a <div>?  Well, yes – and my first version did just that.  But here’s the problem: when you add a <square> element to the DOM you get a <square> element in the DOM.  Your square component’s template (which consists of a div that’s pretty much like the one we’ve been manipulating) appears within the <square> element.

Now, for 99% of purposes this is no big deal, because browsers are supposed to ignore elements that they don’t understand (like <square>).  But it turned out that the Edge browser, which relies on those -ms-grid-row and -ms-grid-column styles to position the square in the grid, can’t understand the div’s grid styling when the div is buried inside a parent element (even though it’s supposed to ignore the parent element).  I had to remove that element from the DOM – and there just isn’t a way to do that when you’re using components and templates.  But by using a directive I was able to manipulate the div element without the need for a parent element.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s