Angular and Focus Setting on New HTML Elements

I had to deal with an interesting problem this weekend.  I was working on a screen for a demo project that was using Angular ngRepeat directive to display multiple fields for a sub-entity of a main screen.  Something similar to the demo below.

image

The behavior I wanted to see is as follows.  When a user click on Add Address button, I want to add a new item to address array inside my model, then scroll new address into the view, then set focus onto first element of newly added address panel.

Of course, adding row was easy – just push a new object into array.  Now, what do we do after that?  I want to follow Angular way and keep HTML manipulations out of my controller, which is where I add an item to the array.  So, I will implement focus setting as a directive.  Now, how do I know what element to set focus to?  I will model this using Angular events which rely on “magic strings”  otherwise known as event names.  I am going to add my own event, but have another string parameter, where parameter would be control name.  I cannot easily use control IDs, since I am inside repeat block, so this would duplicate IDs.  Instead, name is pretty safe, I just need to find the last element with the target name.  So, my controller code is easy:

$scope.addAddress = () => {
                if ($scope.model) {
                    var address: IPersonAddress = {
                        PersonAddressId: 0,
                        PersonId: $scope.model.PersonId,
                        AddressId: 0,
                        AddressSystemTypeId: 0,
                        IsBilling: true,
                        IsDeleted: false,
                        AddressLine1: "",
                        AddressLine2: "",
                        AddressLine3: "",
                        City: "",
                        StateId: 0,
                        ZipCode: "",
                        County: ""
                    };
                    $scope.model.PersonAddresses.push(address);
                    var args: app.events.SetFocusArgs = {
                        controlName: "AddressSystemTypeId"
                    };
                    $scope.$broadcast(app.events.AppEvents.FocusRequested, args);
                }
            };

The code above is TypeScript, but you can so easily translate it into JavaScript.  I have specific focus event argument, just to make my code cleaner.  You can just as easily pass in a string.  Now, it is time to write my directive.

This is more complicated for a few reasons.  It takes Angular sometime to process additional row, hence initially my new element does not even exist.  Secondly, I need to find where to scroll to.

I am going to solve first problem by simply using $timeout service and just wait for 200 ms.  Should be more than enough, but you can wait longer if you would like.  Since it is less than typical double-click time, the user should not notice.  After that I am going to use jQuery (or Angualr jqLite) to locate the last element with the specified name.  Then, I am going to call focus() to set focus to it after waiting another short while.  Here is the final version:

    export class SetFocusDirective extends app.directives.BaseDirective {

        constructor($timeout: ng.ITimeoutService) {
            super();

            this.link = function (scope: ng.IScope) {
                var focusHandler = (event: ng.IAngularEvent, ...args: any[]) => {
                    var request: app.events.SetFocusArgs = args[0];

                    $timeout(function () {
                        var control = angular.element("[name=" + request.controlName + "]").last();
                        if (control) {
                            angular.element("body").scrollTop(control.offset().top);
                            $timeout(function () {
                                control.focus();
                            }, 200);
                        }
                    }, 200);
                };
                scope.$on(app.events.AppEvents.FocusRequested, focusHandler);
            };
        }
    }
  angular.module("app.directives.Common", [])
        .directive("setFocus", ["$timeout", function ($timeout: ng.ITimeoutService) {
            return new app.directives.SetFocusDirective($timeout);
        }]);

Now I just need to inject directive into my view, really any element on it.

<div class="panel-group" id="accordion" set-focus>

That is all.  Easy and clean, IMHO.

Enjoy.