1

Similar to this question, I want to set focus on the last <select> whenever it gets added. As there's a single method doing it, I need no directive and no watch and no events. My function

$scope.addNew = function() {
    $scope.items.push({});
    $timeout(function() {
        $("select").focus();
    });
};

works nicely, except when called directly from the controller function definition like

angular.module('myModule').controller('MyCtrl', function($scope, $timeout) {
    $scope.items = {};
    ...
    $scope.addNew();
}

It looks like the timeout happens before the DOM gets constructed and $("select") is empty. With a delay of some 100 ms it works again, but this is a bad hack.

Contrary to what's said in the answer to the linked question, timeout doesn't suffice.

So what's a reliable way to wait for angularjs being really done with the DOM and everything?

Update:

It probably doesn't work because of the select to be focused being embedded in directives (including ng-repeat and some own ones) That's why there initially was no DOM element to focus on.

According to the comments, I need a directive. What's unclear is how exactly to do it. I tried and failed and found out a simpler solution.

What I need

I wasn't very explicit with this, so let me clarify.

  • I'm working with a table where each row contains some editable fields.
  • In addNew, I want to set focus on the first editable field of the new row.
  • In my case this happens to be the very last select.

It worked except at the very beginning, when I was adding the very first row from the controller body.

Why I'm opposed to using a directive

To my limited understanding, it's completely backwards:

  • A directive modifies the look, behavior, or structure of a given element. But there's no element which should be modified. I tried to put a directive on everything from the select itself to the whole body.
  • It needs to watch something or listen to an event, but I only want to invoke a function manually.
  • It didn't work (for me and others as the comments to the linked question shows).
Community
  • 1
  • 1
maaartinus
  • 44,714
  • 32
  • 161
  • 320
  • 1
    This would work best in a directive. That is where DOM manipulation should be performed, and in this case, putting the focus function inside the link function would work without a wonky timeout. – Ben Felda May 11 '14 at 02:21
  • @BenFelda: I love directives... but watching `items` seems wasteful and posting an event when I exactly know both the caller and the callee feels strange (I'm trying to use events sparingly as they're pretty powerful and losing track of them is too easy). – maaartinus May 11 '14 at 03:11
  • you could also use ng-init on the element that gets added
    will only get called once the DOM has rendered the element
    – btm1 May 11 '14 at 03:41
  • It seems to work [here](http://plnkr.co/edit/zZzyAS1tJUbyIm0fX8fO?p=preview) with `$timeout` and no delay. What is different between that Plunker and your code? – Marc Kline May 11 '14 at 04:38
  • @MarcKline: There are many differences and I'm not sure which one is relevant. You're extending an existing `select`, while I'm adding a new table row containing a new one, but this probably doesn't matter. My DOM is embedded in a directive, which probably makes the difference, see my edit. – maaartinus May 12 '14 at 00:33

3 Answers3

2

I am going to try and influence you to use a directive here, just to perform the behavior.
Here is a fiddle.

Basic premise is adding the behavioral directive to the element inside repeater:

<table>
     <tr ng-repeat="item in items">
         <td>{{item}}: <input type="text" auto-focus/></td>
     </tr>
</table>

Then your directive would put focus on the last added element:

app.directive('autoFocus', function(){ 
    return function link(scope, elem){
        elem[0].focus();   
    }
});

No watchers or events needed unless I am missing something that you require.

Ben Felda
  • 1,474
  • 10
  • 15
  • This is pretty simple and it actually works (not only in your fiddle but also in my code, where all the complicated stuff did not). I guess for now I'll stick with my own solution and will keep yours in mind for the case when I need it to happen automatically. – maaartinus May 12 '14 at 01:02
1

Code that manipulates the DOM should go in a directive, but if you switch to a directive and still have reason to wait until Angular is finished updating the scope and the dom, use $scope.$evalAsync:

$scope.$evalAsync( function() { 
  // This will wait until Angular is done updating the scope
  // Do some stuff here
  //
});
JimKeller
  • 36
  • 2
1

The solution was very trivial: Instead of calling $scope.addNew(); directly, I put it in $scope.init invoked from <form ng-init="init()">.

According to the documentation

The only appropriate use of ngInit is for aliasing special properties of ngRepeat, as seen in the demo below. Besides this case, you should use controllers rather than ngInit to initialize values on a scope.

this seems to be wrong (or maybe not, as ngRepeat si involved). I'm only using it to postpone the call to $scope.addNew();, where neither timeout nor posting events worked.

maaartinus
  • 44,714
  • 32
  • 161
  • 320