为什么从控制器修改DOM是错误的?

时间:2016-12-12 09:56:37

标签: angularjs dom controller

对于任何具有一点经验的Angular用户而言,直接从控制器直接修改DOM是不好的做法。一个原因是它导致DOM操作代码被本地化并且不能被重用。从这个优秀的Toptal Angular tutorial开始,还有两个原因不能从控制器中操作DOM:

  • 违反了控制器的目的,
  • 已转换的子内容尚未添加到DOM


第一个原因是显而易见的,因为控制器只是那个,即代理视图和模型之间的信息交换的实体。但是,我不明白第二点是什么意思。

这个问题实际上是由我目前面临的真正的UI问题所驱动的。我的团队中的开发人员(他将保持无名)实际上将代码添加到我们的一个控制器中,这些控制器直接操作DOM。以下是感兴趣区域的小屏幕截图:

Enter image description here

点击铅笔会将我的名字变成可编辑的文本框。但是,在某些浏览器中,我们会看到瞬间闪烁,其中所有上述DOM元素似乎都跳到了整个地方。似乎发生了一些很奇怪的事情,我想了解它是什么。

以下是上述表单中HTML的简化版本:

<input class="usrProfileTextBox" id="displayNameTextBox"
       ng-show="editingDisplayName" type="text" ng-model="displayName"
       ng-keyup="$event.keyCode == 13 ? changeDisplayName() : null"
       ng-blur="changeDisplayName()" />
<div class="usrProfileRightCol">
    <label class="usrProfileNameLabel" ng-hide="editingDisplayName">{{displayName}}
    </label>
    <span ng-hide="editingDisplayName" ng-click="showEditDisplayName()"
          class=" pointer-click glyphicon glyphicon-pencil"></span>
</div>

并且,为了完整性,这里是控制器功能,当用户单击用户名框的编辑按钮时会触发该功能:

$scope.showEditDisplayName=function(){
    $scope.previousDisplayName = $scope.displayName;
    $scope.editingDisplayName = true;
    $timeout(function() {
        document.getElementById("displayNameTextBox").value=$scope.displayName;
        document.getElementById("displayNameTextBox").focus();
    });
}

当我们的控制器直接操作DOM时,任何人都可以了解实际发生的事情吗?

2 个答案:

答案 0 :(得分:1)

第一个问题是关注点的分离。角色根据指令解剖分布。控制器定义范围内的东西(对于controllerAs语法是this),链接函数操纵DOM并将所有控制器与事物联系在一起。

第二个问题是directive compilation precedence。子元素DOM元素可能在控制器构造函数和绑定中不可用。角色分配表明这些东西在编译,预链接和后链接功能中效果最好。

这是Angular 1.4.x及更低版本的既定社区实践。由于引入了组件以及Angular 1和Angular 2之间的融合过程,Angular 1.5中的情况发生了很大变化。

由于组件中没有链接功能,因此可以在控制器中使用lifecycle hooks实现它们。来自不使用require d控制器的链接函数的所有代码分别进入钩子 - 从预链接到$onInit钩子,从后链接到$postLink钩子。

问题中列出的代码段中没有优先级问题,因为DOM修改是在点击时运行而不是在初始化时运行。问题是使用ng-controller指令而不是自定义指令/组件的层次结构。在精心设计的应用程序中,可能根本没有ng-controller指令。

document.getElementById有代码味道,因为它会影响可测试性。此代码可能根本不需要DOM操作,因为使用数据绑定和ng-focus指令可能会实现相同的操作。

在控制器/范围方法中不允许任何DOM操作本身并不是目的(如果应该在ng-click上进行DOM操作,显然应该在某些方法中完成)。当事情以一种惯用于Angular的方式完成时,它们就可以避免。

答案 1 :(得分:1)

视图将在依赖视图中的$scope参数更改后重新编译。在你的情况下它是editingDisplayName。我们遇到了同样的问题,我们可以通过以下方式完成这项工作:

  1. 处理separat $scope变量中两个项目的显示状态。
  2. 使用最少的超时延迟。
  3. 问题的根源是使用editingDisplayName重新编译。它不是同时在两个元素的一个渲染步骤中设置的。由于ng-hide的JavaScript的异步进行,这会导致小的闪烁。这也可能发生在指令或本机JavaScript中。它不依赖ng-controller

    检查以下尝试。它处理两个showState中元素的$scopes。多数民众赞成我们如何解决这种闪烁问题。 (由于你的模糊功能,你可能需要改进这个例子)

    视图

    <input class="usrProfileTextBox"
           id="displayNameTextBox"
           ng-show="showInputBox"
           type="text"
           ng-model="displayName"
           ng-keyup="$event.keyCode == 13 ? changeDisplayName() : null"
           ng-blur="changeDisplayName()" />
    
    <div class="usrProfileRightCol"
         ng-show="showTextBox">
        <label class="usrProfileNameLabel">
               {{displayName}}
        </label>
        <span ng-click="showEditDisplayName()"
              class="pointer-click glyphicon glyphicon-pencil">
        </span>
    </div>
    

    控制器

    $scope.showEditDisplayName = function(){
    
        $scope.previousDisplayName = $scope.displayName;
        $scope.showTextBox = false;
    
        $timeout(function () {
            document.getElementById("displayNameTextBox").value=$scope.displayName;
            document.getElementById("displayNameTextBox").focus();
            $scope.showInputBox = true;
        }, 100);
    };
    

    您可能会使用AngularJS指令替换本机JavaScript集valuefocus。但这只是一个重构暗示。

    Tim的编辑:

    这个答案的主旨基本上解决了这个问题,但实际导致闪烁的函数是changeDisplayName(),当用户模糊输入框时会调用该函数,该输入框应隐藏该框并显示更新的用户名标签。这是我最终在生产中使用的代码。注意小心使用定时器分隔输入框的隐藏(第一个),然后显示用户名标签(第二个):

    $scope.changeDisplayName=function(){
        if ($scope.showInputBox == false) {
            return; // prevent function from being called twice from both keypress
        }           // and blur events
    
        // hide the input box, pause, then show the username label
        $scope.showInputBox = false;
        $timeout(function() {
            $scope.showDisplayLabel = true;
        }, 100);
    }