Creating An Input-Driven AutoFocus Directive In Angular 5.0.2

HTML form elements already provide for an "autofocus" attribute that will pull focus to an input field after it is rendered on the page. This is great for static pages; but, in my experience, using the autofocus attribute in an Angular 5 application can be a bit hit-and-miss. It will often work the first time that an input is rendered; but, it will then stop working even if that input is hidden and re-rendered. As such, I usually end-up creating an autofocus attribute directive that wraps the focus workflow in a timer. Encapsulating the autofocus in an attribute directive has the added benefit of being able to (somewhat) programmatically control which input receives focus.

Most of the time, I just want to improve the behavior of the native "autofocus" attribute. So, I use "[autofocus]" as my directive's selector. But, at the same time, I like to provide some sort of input-driven behavior for certain use-cases. So, I usually provide an alternate selector like [appAutofocus] that can accept a property-value. This way, the full selector of my Angular 5 attribute directive looks like this:

selector: "[autofocus], [appAutofocus]"

... with inputs that look like this:

inputs: [ "appAutofocus" ]

This way, the "autofocus" is still treated like a stand-alone attribute. And, if I want to provide a input property value, I have switch over to the custom attribute. I think this branching strategy leads to less surprise in the code.

Now, to be honest, I don't actually understand why the native autofocus attribute stops working consistently in a dynamic single-page application (SPA). All I know is that wrapping the focus workflow in a small timer seems to help smooth out the wrinkles. In fact, the bulk of the autofocus attribute directive is just managing the timer workflow. Ultimately, we're just calling HTMLElement.focus() under the hood.

Because our attribute directive has two different modes of input consumption, we're using both the ngOnChanges() and the ngAfterContentInit() life-cycle hook methods. This way, we can use the ngAfterContentInit() for the stand-alone attribute; and, we can use the ngOnChanges() for the data-driven input bindings.

Now, to see this attribute directive in action, I've created a demo in which you can toggle the display of two different sets of inputs. The first set uses the normal [autofocus] attribute. And, the second set uses the [appAutofocus] attribute with a view-model driven input binding. This way, when we toggle the second set of inputs, we can control which input receives focus:

As you can see, the second set of inputs all use a property-binding like:

[appAutofocus]="( focus === 'one' )"

When using this special kind of syntax, we can programmatically control which input in a group receives the focus when the parent context is rendered. And, if we run this app in the browser and toggle the "two" input, we get the following output:

As you can see, we can use the view-model to declaratively define which input will receive focus.

For the most part, I use this Angular 5 attribute directive to make sure that the [autofocus] behavior performs more consistently as I hide and show elements in a single-page application (SPA). But, the data-driven aspects of this directive also help to focus different elements based on the state of the page (though, admittedly, this second mode is a bit more theoretical for me at the moment).

This way, if the [autofocus] attribute appears on a read-only input, it will attempt to select the text. This feels like an appropriate gesture since a read-only element would likely only have an auto-focus intent if the gesture was to allow the user to select/copy the read-only content.

expect(inputEl).toBe(fixture.debugElement.query(By.css(':focus'))); });});I've tried may variants of the above and the focus element is always null, the directive works in the app and is called by the test...Me thinks it's a timing thing.

I am not too familiar with testing (to be honest). I wonder if the setTimeout() is somehow causing an issue? There is, by default, a 10ms delay between the initiation of focus and the actual call to .focus(). Perhaps the test doesn't know to wait for that?

Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please
do not post unrelated questions or
large chunks of code. And, above all, please be nice to each other - we're trying to
have a good conversation here.

I am the co-founder and lead engineer at InVision App, Inc — the world's leading prototyping,
collaboration & workflow platform. I also rock out in JavaScript and ColdFusion 24x7 and I dream about
promise resolving asynchronously.