You’ve probably heard it a thousand times now. “AngularJS teaches HTML new tricks.” The way it does that is through directives. In my last related post I covered how to build a testable filter. Directives can be tested in a similar fashion, but what happens when they have to interact with the rest of your application? Instead of teaching your controllers how to talk to UI components, or overloading the $scope object, look to services as the mortar to hold pieces of your app together.
The console in my 6502 simulator is rendered with a single directive:
<div class="column"><console></console></div>
Instead of walking you through the directive, however, I’d like to start with the specs for the console. Here are the relevant specs:
The size of the buffer is taken from a set of global constants that are configurable and will automatically update the tests. That’s it – that’s all I want from my console service. Notice that I don’t care how it is rendered yet, I’m just concerned about how the rest of my app will interface with it.
From the tests I was able to determine an interface for the service:
export interface IConsoleService {
lines: string[];
log(message: string);
}
And from the interface I could then easily implement a class that would pass the tests:
export class ConsoleService implements IConsoleService {
public lines: string[];
constructor() {
this.lines = [];
}
public log(message: string) {
this.lines.push(message);
if (this.lines.length > Constants.Display.ConsoleLines) {
this.lines.splice(0, 1);
}
}
}
Now any other component that needs to write to the console can simply have the service injected and log a message like this:
this.consoleService.log("CPU has been successfully reset.");
What’s nice about AngularJS is that we now have a fully functional app even though we haven’t even considered the UI yet. For the UI, we’ll use a directive. The directive will have the console service injected and will watch it for changes. The template was simple enough I decided to inline it. It is simply a styled divider with a collection of spans that are separated by newlines. The directive sets the scope to the array of messages. This automatically sets angular to watch for changes and will refresh/redraw the console when needed. The only other piece I added was my own watcher to scroll the div so that the top is always displayed (otherwise you will just see the scrollbar grow while you are looking at the oldest lines). The entire setup is implemented like this:
public static Factory(
consoleService: Services.ConsoleService) {
return {
restrict: "E",
template:
"<div class='console'><span ng-repeat='line in lines'>{{line}}<br/></span></div>",
scope: {},
link: function (scope: IConsoleScope, element) {
var e: JQuery = angular.element(element);
scope.lines = consoleService.lines;
scope.$watch("lines", () => {
var div: HTMLDivElement = <HTMLDivElement>$(e).get(0).childNodes[0];
$(div).scrollTop(div.scrollHeight);
}, true);
}
};
}
None of the components have any dependency on the console directive, so it can be changed, updated, or manipulated without impacting the rest of the app. The directive itself doesn’t even have a direct dependency on the console service – instead, the factory that returns the service has the service injected and uses it to set up the directive. You could easily mock or stub a simpler implementation of the console service to test out the directive.
For a more involved example, take a look at the display service and directive. This is a little more involved because the directive uses SVG to render a bung of rectangles that are then colored according to the corresponding memory address. A separate palette component is used to generate the palette. Even with this more complex configuration, the CPU service simply informs the display service when memory changes without any knowledge of how the display is being rendered. If I wanted to, I could switch to a canvas-based implementation and only have to change one component within the application.
Although you can browse the full JavaScript source on the simulator site, the TypeScript project is available at CodePlex.