在AngularJS中轻松进行DOM操作 - 单击按钮,然后将焦点设置到输入元素

19

我有这段Angular代码:

<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>

我希望发生的事情:当用户点击按钮时,输入元素将获得焦点。

在单击按钮元素并将其聚焦后,如何找到输入元素?

我可以编写以下类似的函数:

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();
}

他们两个都不起作用 - 有没有更好的Angular方式来解决这个问题?像jQuery中使用.closest().find()这样的函数吗?

更新:

我发现这种方法可行(但仍然不像正确的解决方案)

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

我正在使用setTimeout将其包装起来,这样在Angular完成所有操作后,它会聚焦于输入元素。


你应该查看第二个包装器,而不是第一个。 - Pete
@Pete,我错误地写了错误的类名。我已经修正了。这不是查找元素的问题,而是如何正确使用AngularJS的问题。 - Alon
你能提供一个 Fiddle 来展示你想要实现的内容吗? - ganaraj
第二个hack非常丑陋,相反,你应该使用$timeout而不是setTimeout(虽然我不建议这样做)。 - NicoJuicy
7个回答

38

应该将DOM操作放在指令中而不是控制器中。 我会定义一个focusInput指令,并在按钮上使用它:

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

指令:

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

由于jqLite的DOM遍历方法相对有限,所以我不得不使用parent().parent()。你可以选择使用jQuery或其他JavaScript方法。

正如你已经发现的那样,需要使用$timeout,以便在浏览器渲染完成(即完成处理单击事件)后调用focus()方法。

find('input')[0]使我们可以访问DOM元素,从而使用JavaScript的focus()方法(而不是使用需要jQuery的find('input').focus()方法)。


1
感谢指令示例。我有一个小问题:如果该指令只被使用一次,将其作为指令是否有点过重? - Freewind
8
@Freewind,任何DOM操作(遍历、focus()、添加/删除元素或类)都应该在指令中完成。当然,你可以在控制器中放入一行jQuery代码来完成这个操作,但这样就违反了“关注点分离”的原则。这完全取决于你希望你的架构有多干净。关于“只使用一次”,你可以通过传递一个id、类名或其他属性来使这个指令更加通用/可重用,然后指令就可以找到它。 - Mark Rajcok
尽管以“Angular方式”做所有事情并在此处使用$timeout很诱人,但我认为出于两个原因你不应该这样做:1)如果您不更改模型,并且更重要的是2)它将触发一个完全不必要的$digest,当在另一个指令(可能应用于输入)中处理焦点事件时,您无法调用scope.$apply(),因为它会告诉您已经进行了应用。 - lex82
@MarkRajcok,如果我想要类似的东西,并且想要在输入位置上有更多的自由度,那么将按钮的id或类名作为指令属性提供是否是一个好的做法?如果我需要针对其他DOM元素进行操作,我也可以像这样将它们提供给指令吗? - user1620696
1
@user1620696,是的,没错。指令属性是将信息传递给指令的好方法。 - Mark Rajcok
3
现在,我热爱使用Angular框架,并将其用于一些相当复杂的应用程序。这种只为非常简单的DOM操作编写一个指令的类型是臃肿的。如果您喜欢,整个东西可以用一行更易读的原始JavaScript或jQuery实现。 - FlavorScape

10

最近我在研究AngularJS,遇到了一个类似的情况。

我正在更新主angular页面上的Todo示例应用程序,以添加“编辑”模式,当您双击待办事项时即可进入该模式。

我能够使用基于模型/状态的方法解决我的问题。如果您的应用程序以类似的方式工作(即当模型的某些条件为真时,您希望将焦点设置在字段上),那么这种方法可能也适用于您。

我的方法是在用户双击待办事项标签时将model.editing属性设置为true——这将显示可编辑输入框并隐藏常规的不可编辑标签和复选框。我们还有一个名为focusInput的自定义指令,它对同一model.editing属性进行监视,并在值更改时将焦点设置在文本字段上:

<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>

这里是focusInput指令,当某些条件为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
                        });
                    });

                });

            };
        }
    ]);

这对我有效,但我不得不将"element.select();"更改为"element[0].select();"。 - Antony Scott

5
以下是触发目标dom元素焦点事件的指令:
AngularJs指令:
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);
                    }
                });
            });
        }
    };
});

HTML:

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

在Plunker上有一个实时的例子,您可以查看链接

4
我不得不创建一个帐户来提供简单的答案。
//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">
...

确保在不使用输入时重置此变量。您可以轻松添加ng-blur指令以将其更改回来,或者另一个ng-click事件将其重置为false。将其设置为false只是为下一次做好准备。如果您找不到示例,请查看我找到的ng-blur指令示例。

.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});
        });
    });
    }
}]);

值得注意的是,这假定已加载jQuery。如果没有jQuery,则需要将element.focus();更改为element[0].focus(); - Ade
我必须为focusBool指令指定$timeout依赖项,才能使其正常工作。这个解决方案最小限度地满足了我的需求。谢谢。 - Jigar

4
这是我想到的翻译。我以Mark Rajcok的解决方案为基础,然后进行了改进以便于重复使用。它可以配置,并且不需要在您的控制器中编写任何代码。重点是纯呈现方面,不应该需要控制器代码。 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>

指令:

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();
                });
            });
        }
    };
});

假设:

  • 您的赠送者和接收者在附近。
  • 当在同一页上多次使用时,所使用的id是唯一的,或者赠送者和接收者位于DOM的独立分支中。

0

要找到输入,请添加它的ID <input id="input{{$index}}" .. /> 并将ngRepeat索引作为参数传递给函数 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>   

在编程中,使用零延迟的$timeout函数等待DOM渲染结束。然后可以在$timeout函数中使用getElementById查找输入。别忘了将$timeout添加到控制器中。
.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();
    });
  }
});

0

关于使用 .closest() 方法,我建议您应用原型继承机制来扩展 Angular 的功能。就像这样:

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

标记语言:

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

使用方法:

 $scope.removeNote = ($event)->
    currentNote = angular.element($event.currentTarget).closest("content-list_item") 
    currentNote.remove()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接