19

I have this angular code:

<div class="element-wrapper" ng-repeat="element in elements">
  <div class="first-wrapper">
     <div class="button" ng-click="doSomething(element,$event)">{{element.name}}</div>   
  </div>
  <div class="second-wrapper">
    <input type="text" value="{{element.value}}">    
  </div>
</div>

What I want to happen: when the user clicks the button - the input element will be focused.

How do I find the input element after I click the button element and focus it?

I can do a function that looks like this:

function doSomething(element,$event) {
  //option A - start manipulating in the dark:
  $event.srcElement.parentNode.childNodes[1]

  //option B - wrapping it with jQuery:
   $($event.srcElement).closest('.element-wrapper').find('input').focus();
}

Neither of them work - Is there a nicer Angular way to do it? Using functions such as .closest() and .find() as in jQuery?

Update:

I found this hack to be working (but it still doesn't seem like the correct solution):

function doSomething(element,$event) {
   setTimeout(function(){
     $($event.srcElement).closest('.element-wrapper').find('input').focus();
   },0)
}

I am wrapping it with setTimeout so after Angular finishes all of its manipulations it focuses on the input element.

Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
Alon
  • 7,618
  • 18
  • 61
  • 99
  • you should be looking in the second wrapper - not the first – Pete Mar 20 '13 at 08:50
  • @Pete, I mistakenly wrote the wrong class name. I fixed it. this is not an issue of finding the element but of how to do it properly with AngularJS – Alon Mar 20 '13 at 09:06
  • Can you provide a fiddle with what you are trying to achieve? – ganaraj Mar 20 '13 at 09:15
  • The second hack is really ugly, alternatively, you should use $timeout instead of setTimeout ( i wouldn't advice it though) – NicoJuicy Apr 02 '15 at 09:57

7 Answers7

38

DOM manipulation should be in a directive instead of the controller. I would define a focusInput directive and use it on the button:

<div class="button" focus-input>{{element.name}}</div>   

Directive:

app.directive('focusInput', function($timeout) {
  return {
    link: function(scope, element, attrs) {
      element.bind('click', function() {
        $timeout(function() {
          element.parent().parent().find('input')[0].focus();
        });
      });
    }
  };
});

Plunker

Since jqLite is rather limited in terms of DOM traversal methods, I had to use parent().parent(). You may wish to use jQuery or some JavaScript methods.

As you already found out, $timeout is needed so that the focus() method is called after the browser renders (i.e., finishes handling the click event).

find('input')[0] gives us access to the DOM element, allowing us to use the JavaScript focus() method (rather than find('input').focus() which would require jQuery).

Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • 1
    Thanks for directive sample. I have a small question: If the directive is just be used once, is it a little heavy to make it a directive? – Freewind Mar 21 '13 at 16:13
  • 8
    @Freewind, any kind of DOM activity (traversal, focus(), add/remove elements or classes) should be done in a directive. Sure, you could put one line of jQuery into a controller to do this, but you're then violating "clean separation of concerns". It all depends on how clean you want your architecture to be. Regarding "used once", you could make this directive much more generic/reusable by passing in an id or classname or something else as an attribute, and then the directive could find that. – Mark Rajcok Mar 21 '13 at 16:35
  • Although it is tempting to do everything "the angular way" and use $timeout here, I think you should not do it for two reasons: 1) It will trigger a $digest which is completely unnecessary, if you don't change the model and more important 2) you cannot call scope.$apply() when handling the focus event in another directive (maybe applied to the input), because it will tell you that apply is already in progress. – lex82 Nov 22 '13 at 17:25
  • @MarkRajcok, in the case I wanted something like that and wanted to have a bit more freedom about where the input is, would it be a good practice to provide an id or classname of the button as attributes to the directive? And in any other cases I needed to target other DOM elements I could provide them to the directive like that? – user1620696 Jun 03 '14 at 11:03
  • 1
    @user1620696, yes and yes. Directive attributes are a good way to pass information to a directive. – Mark Rajcok Jun 05 '14 at 02:49
  • 3
    Now, I love angular and use it for some quite complex applications. This type of one-directive-for-a-very-simple-dom-manipulation is just bloatware. That whole thing is one line of more readable vanilla js or jQuery, if that's your fancy. – FlavorScape Aug 25 '14 at 23:26
10

I've been having a look at AngularJS recently and came across a similar situation.

I was working to update the Todo example application from the main angular page to add an "edit" mode when you double click on a todo item.

I was able to solve my issue using a model/state-based approach. If your application works in a similar way (you want to set focus on a field when some condition on the model is true) then this might work for you too.

My approach is to set the model.editing property to true when the user double-clicks on the todo label - this shows the editable input and hides the regular non-editable label and checkbox. We also have a custom directive called focusInput that has a watch on the same model.editing property and will set focus on the text field when the value changes:

<li ng-repeat="todo in todos">

    <div>
        <!-- Regular display view. -->
        <div ng-show="todo.editing == false">
            <label class="done-{{todo.done}}" ng-dblclick="model.editing = true">
                <input type="checkbox" ng-model="todo.done"/>{{todo.text}}
            </label>
        </div>

        <!-- Editable view. -->
        <div ng-show="todo.editing == true">
            <!--
                - Add the `focus-input` directive with the statement "todo.editing == true".
                  This is the element that will receive focus when the statement evaluates to true.

                - We also add the `todoBlur` directive so we can cancel editing when the text field loses focus.
            -->
            <input type="text" ng-model="todo.text" focus-input="todo.editing == true" todo-blur="todo.editing = false"/>
        </div>
    </div>

</li>

Here is the focusInput directive that will set focus on the current element when some condition evaluates to true:

angular.module('TodoModule', [])
    // Define a new directive called `focusInput`.
    .directive('focusInput', function($timeout){
        return function(scope, element, attr){

            // Add a watch on the `focus-input` attribute.
            // Whenever the `focus-input` statement changes this callback function will be executed.
            scope.$watch(attr.focusInput, function(value){
                // If the `focus-input` statement evaluates to `true`
                // then use jQuery to set focus on the element.
                if (value){
                    $timeout(function(){
                        element.select();
                    });
                }
            });

        };
    })
    // Here is the directive to raise the 'blur' event.
    .directive('todoBlur', [
        '$parse', function($parse){
            return function(scope, element, attr){

                var fn = $parse(attr['todoBlur']);
                return element.on('blur', function(event){

                    return scope.$apply(function(){
                        return fn(scope, {
                            $event: event
                        });
                    });

                });

            };
        }
    ]);
Sly_cardinal
  • 12,270
  • 5
  • 49
  • 50
5

Here is a directive that triggers a focus event on a target dom element:

AngularJs Directive:

app.directive('triggerFocusOn', function($timeout) {
    return {
        link: function(scope, element, attrs) {
            element.bind('click', function() {
                $timeout(function() {
                    var otherElement = document.querySelector('#' + attrs.triggerFocusOn);

                    if (otherElement) {
                        otherElement.focus();
                    }
                    else {
                        console.log("Can't find element: " + attrs.triggerFocusOn);
                    }
                });
            });
        }
    };
});

The html:

<button trigger-focus-on="targetInput">Click here to focus on the other element</button>
<input type="text" id="targetInput">

A live example on Plunker

Lesh_M
  • 596
  • 8
  • 6
4

I had to create an account just to provide the easy answer.

//Add a bool to your controller's scope that indicates if your element is focused
... //ellipsis used so I don't write the part you should know
$scope.userInputActivate = false;
...
//Add a new directive to your app stack
...
.directive('focusBool', function() { 
    return function(scope, element, attrs) {
        scope.$watch(attrs.focusBool, function(value) {
            if (value) $timeout(function() {element.focus();});
        });
    }
})
...

<!--Now that our code is watching for a scope boolean variable, stick that variable on your input element using your new directive, and manipulate that variable as desired.-->
...
<div class="button" ng-click="userInputActivate=true">...</div>
...
<input type="text" focus-Bool="userInputActivate">
...

Be sure to reset this variable when you aren't using the input. You can add an ng-blur directive easy enough to change it back, or another ng-click event that resets it to false. Setting it to false just gets it ready for next time. Here is an ng-blur directive example I found in case you have trouble finding one.

.directive('ngBlur', ['$parse', function($parse) {
    return function(scope, element, attr) {
        var fn = $parse(attr['ngBlur']);
        element.bind('blur', function(event) {
        scope.$apply(function() {
            fn(scope, {$event:event});
        });
    });
    }
}]);
  • It's worth noting that this assumes that jQuery has been loaded. Without jQuery `element.focus();` needs to be changed to `element[0].focus();` – Ade Feb 27 '14 at 12:27
  • I had to specify $timeout dependency for focusBool directive, for this to work. This solution suits my needs minimally. Thanks. – Jigar Aug 09 '14 at 11:06
4

Here is what I have come up with. I started with Mark Rajcok's solution above and then moved to make it easy to re-use. It's configurable and does not require any code in your controller. Focus is pure presentation aspect and should not require controller code

html:

 <div id="focusGroup">
     <div>
         <input type="button" value="submit" pass-focus-to="focusGrabber" focus-parent="focusGroup">
     </div>
     <div>
         <input type="text" id="focusGrabber">
     </div> 
 </div>

directive:

chariotApp.directive('passFocusTo', function ($timeout) {
    return {
        link: function (scope, element, attrs) {
            element.bind('click', function () {
                $timeout(function () {
                    var elem = element.parent();
                    while(elem[0].id != attrs.focusParent) {
                        elem = elem.parent();
                    }
                    elem.find("#"+attrs.passFocusTo)[0].focus();
                });
            });
        }
    };
});

assumption:

  • Your giver and taker are close by.
  • when using this multiple times on one page id's used are unique or give and taker are in an isolated branch of the DOM.
Florian Hehlen
  • 103
  • 1
  • 4
0

For using .closest() method I suggest that You apply prototype inheritance mechanism fox expand angular opportunities. Just like this:

angular.element.prototype.closest = (parentClass)->
  $this = this
  closestElement = undefined
  while $this.parent()
    if $this.parent().hasClass parentClass
      closestElement = $this.parent()
      break
    $this = $this.parent()
  closestElement

Markup:

<span ng-click="removeNote($event)" class="remove-note"></span>

Usage:

 $scope.removeNote = ($event)->
    currentNote = angular.element($event.currentTarget).closest("content-list_item") 
    currentNote.remove()
Filipp Gaponenko
  • 784
  • 6
  • 10
0

To find input add it Id <input id="input{{$index}}" .. /> and pass ngRepeat index as parameter to function ng-click="doSomething(element,$event, $index)"

 <div class="element-wrapper" ng-repeat="element in elements">
  <div class="first-wrapper">
     <div class="button" ng-click="doSomething(element,$event, $index)">{{element.name}}</div>   
  </div>
  <div class="second-wrapper">
    <input id="input{{$index}}" type="text" value="{{element.value}}">    
  </div>
</div>   

In function use $timeout with zero delay to wait to the end of DOM rendering. Then input can be found by getElementById in $timeout function. Don't forget to add $timeout to controller.

.controller("MyController", function ($scope, $timeout)
{
  $scope.doSomething = function(element,$event, index) {
    //option A - start manipulating in the dark:
    $event.srcElement.parentNode.childNodes[1]

    $timeout(function () 
    {
      document.getElementById("input" + index).focus();
    });
  }
});