In the tutorial Build a Real-Time Chat Web App with AngularDart and Firebase, I included a couple of attribute directives to help make the UI feel a bit nicer. One of them made sure the chat viewport scrolled to the bottom with the arrival of each new chat message. The other tried to keep the keyboard focus on the chat message input element, and that one will be the focus of this examination.
I teased in the other article that I might write up something on directives, since there wasn't time or space to get into them there, but I admit to another motive. I've run into a lot of other developers' blog posts about attribute directives, and almost invariably, they invent some overly simplistic, contrived, and often useless example to demonstrate the concepts. Even though the directive we'll examine here is exceedingly simple, it's one that served a useful purpose in an actual app, and I wanted at least one directive tutorial out there to show you something you could actually use.
Code tested with Dart SDK 1.24.2 and AngularDart 4.0.0.
What's a Directive For?
Angular's attribute directives are meant to help you alter the appearance, behavior, or both on a host element. I like to use them to keep DOM manipulation code out of my view models. In this case, I had initially peppered my main component's Dart file (the class behind the component view) with calls to chatInput.focus()
. Not only did this require the use of a @ViewChild
decorator and the implementation of AfterViewInit
, but it was crazy how many places required that focus()
call. It was cluttering up my code, so I decided to use a directive, vuHoldFocus
. In the end, directives allowed me to move all of that crass DOM code out of my view model, and the app is better for it. No more @ViewChild
decorators, and no more poking at the innards of the DOM.
Using the Directive
Once it's built, you can convince any DOM element to try to hold onto keyboard focus by applying an attribute:
<input vuHoldFocus>
Building the Directive
And here's the directive you've all been waiting for:
directives/vu_hold_focus.dart
import 'dart:html';
import 'package:angular/angular.dart';
@Directive(selector: "[vuHoldFocus]")
class VuHoldFocus {
Element _el;
VuHoldFocus(Element this._el);
@HostListener('blur')
void onBlur() {
_el.focus();
}
}
Imports
You need to import dart:html
to gain access to Dart's Element
class. Since you could apply this directive to any HTML element, and they all derive from Element
, there's no need to be any more specific. You might be tempted to use InputElement
, because an <input>
is probably the best candidate to be host to a vuHoldFocus
, but it's better not to limit your options.
The angular.dart
file of AngularDart contains definitions for decorators like @Directive
, among other things.
Directive
This is where all the Angular magic comes in. Adding the @Directive
decorator before your directive's class definition tells the framework that it needs to create an instance of this class every time it encounters vuHoldFocus
as an attribute on an element. In the context of the directive's class, the DOM element to which the attribute has been applied is the host.
Properties
VuHoldFocus
has just one private property where you'll store a reference to the host element. You need this in order to call its focus()
method.
Constructor
The class constructor makes use of Angular's dependency injection system to get a reference to the directive's host element. Whichever element you add the vuHoldFocus
attribute to is the host.
An Event Listener
Using another Angular decorator, @HostListener
, you can add an event listener to the directive's host. In this case, you listen to the blur
event, which is thrown every time the element loses focus. The handler, onBlur()
, sets the focus back on the host element.
Caution!
This is a pretty heavy-handed way to manage focus, and you wouldn't want to use it all over your UI, but for the simple chat app from the prior tutorial, it works out quite well. If the user types up a new chat message, then clicks the Send button, that button takes the keyboard focus, and the user has to navigate back to the message input in order to type another. Very annoying. With the attribute directive in place, each time the input element (the host) loses focus, it attempts to take it back.
What's Next?
Well, there you have it. A simple, but still useful, example of Angular's directive feature. Look around your components for code that pokes and prods the DOM. Wouldn't that code be cleaner if all it had to do was manipulate class properties in order to trigger bindings? Yes...yes it would.
The best part of doing this kind of stuff in a directive is that you can reuse it. The next time you need similar functionality, you grab your trusty directive and you're done!
Don't forget to import the directive and include it in your component's directives
list, or Angular won't know what to do with the attribute when it comes across it. That should look something like this:
import 'directives/vu_hold_focus.dart';
...
@Component(
selector: 'my-app',
directives: const [VuHoldFocus]
)