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.

5 Comments

  1. How about inserting an item in the array? Or even more edge-case, adding an item in the array which will be presented not at the end of the displayed list.
    I currently have a situation where the items in my array are associated to items in another array. I’m looking for a method that depends a little less on presentation, any thoughts on that?

  2. In my use case I add items to the end of the array, and the code I show works fine. If you are inserting items in the middle of the array, you would need to know the ID. What you could do is generate a unique ID on the template for the array item and adjust the code to look for control name that is child of the template with that ID. You can just pass ID along with control name in this case. Hope this helps.

  3. Hi Sergey, yes off course, a solution like that will work. I just was wondering whether some ultra-generic solution would be possible, but you will always have some arbitrary decision on what element in the newly added form you’d wish to focus Thanks for your prompt response..

  4. @Robert.
    True you have to decide where to set focus to. You could do something more generic by setting focus to a first controls inside a

    that designates some focus area. In this case you could do $(‘#areaDiv input’).first().focus(). Or you can do a related to button focus $(this).parents(‘div).first().find(‘input’).focus(). At the end, you still will have to make a decision though that is purely dependent on UI conventions that your app is using I think.
    • (ooh, I think you you are missing some Html encoding there)

      This is exact what I did now, some convention between my Angular Javascript and the app, not exactly clean, but gets the job done.

Leave a Reply

Your email address will not be published. Required fields are marked *