Easy dom manipulation in AngularJS - click a button, then set focus to an input element
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.
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).
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
});
});
});
};
}
]);
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
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});
});
});
}
}]);
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.