$ postLink的角度组件/指令运行得太早

时间:2016-05-10 18:52:29

标签: javascript angularjs

更新:我对此问题表示赏心悦目。我不是在寻找黑客或变通办法。我正在寻找以角度组件访问dom的官方方式,并解释为什么我看到的行为($ postLink早期运行)似乎与官方文档相矛盾。
官方文档声明(here):

  

$ postLink() - 在此控制器的元素及其子元素已被链接之后调用。与post-link函数类似,此挂钩可用于设置DOM事件处理程序并执行直接DOM操作

原始问题:我在这里有一个问题的例子 - > http://plnkr.co/edit/rMm9FOwImFRziNG4o0sg?p=preview

我正在使用一个角度组件,我想修改后期链接功能中的dom,但是它不起作用,似乎该功能运行得太早,在模板实际准备好之前在dom之后角度处理。

在html页面中,我有这个:

<my-grid grid-id="'foo'"></my-grid>

该组件定义为:

appModule.component('myGrid',{
    controller: gridController,
    bindings: {
        "gridId": "<",
    },
    templateUrl: 'gridTemplate'
});

在组件模板中,我有:

<table id='{{$ctrl.gridId}}'>
...

(绑定本身有效,毫无疑问。最终,在html中,表的id是'foo',如预期的那样。)

在控制器中,我有类似的东西:

function gridController($scope, $compile, $attrs) {
    console.log ("grid id is: " + this.gridId); // 'foo'

    this.$postLink = function() {
        var elem = document.getElementById(this.gridId);
        // do something with elem, but elem is null
    }
}

我在调试时看到的是,当执行$ postLink函数时,该表位于dom中,但其id属性仍为{{$ctrl.gridId}}而不是foo,因此document.getElementById()找到没有。这似乎与文档形成鲜明对比 我错过了什么?是否有不同的方法来访问组件中的dom?

更新2:今天我意识到指令的常规链接功能也出现同样的问题,它不仅限于组件。显然我误解了“直接操作DOM”的含义 - 链接函数在与dom分离的元素上运行,因此使用带有选择器的document对象是没用的。

3 个答案:

答案 0 :(得分:21)

有关$postLink()的文档是正确的。它的控制器元素之后被称为它的子元素。这并不意味着您可以立即看到指令的结果。也许它正在调用$http并在结果到达时插入结果。也许它正在注册一个观察者,而这个观察者依次设置结果,就像大多数Angular的内置指令一样。

您的案例中的基本问题是您希望在编译插值之后执行DOM操作,或者更好的是,在注册观察者有时间运行一次之后执行DOM操作。

不幸的是,没有官方方式来执行此操作。尽管如此,有一些方法可以实现这一点,但是你不会在文档中找到它们。

编译插值后运行函数的两种常用方法是

  • 使用$timeout没有延迟(默认为0):$timeout(function() { /* Your code goes here */ });

  • 使用Angular's jqLite

  • 提供的.ready()

在您的情况下,一旦具有给定ID的元素存在,您最好使用观察者来运行函数:

var deregistrationFn = $scope.$watch(() => {
    return document.getElementById(this.gridId);
}, (newValue) => {
    if (newValue !== null) {
        deregistrationFn();

        // Your code goes here
    }
});

最后,在我看来,我相信无论何时你需要等待编译插值,或者某些指令插入它们的值,你都不会遵循 Angular的方式建筑物。在您的情况下,为什么不创建一个新组件myGridTable,它需要myGrid作为父组件,并在那里添加适当的逻辑。这样,每个组件的责任都得到了更好的定义,并且更容易进行测试。

答案 1 :(得分:11)

误解在于角度$摘要循环如何与控制器生命周期事件(以及指令生命周期事件)一起工作。

每个控制器/指令生命周期事件都在$ apply内部调用。因此,DOM仅会更新以反映$digest cycle is completed之后的更改。

后链接函数在链接完所有内容后运行,但是at the end of this function更新DOM以反映任何绑定,因为它是$ digest循环内运行的最后一个函数。这意味着尝试查询DOM以获取受绑定影响的任何内容都不起作用(例如ng-repeat,或者在您的情况下,绑定值)。但正如文档所述,DOM的链接已经运行,因此初始模板已经创建并且可以被操作。

由于您需要在应用绑定后查询DOM,因此需要在当前的$ digest周期完成后运行代码。这意味着使用延迟为0的$timeout,即executed right after the current $digest cycle has ended

function gridController($scope, $compile, $attrs, $timeout) {
    console.log ("grid id is: " + this.gridId); // 'foo'

    this.$postLink = function() {
        $timeout(function() {
            var elem = document.getElementById(this.gridId);
            // do something with elem now that the DOM has had it's bindings applied
        });
    }
}

答案 2 :(得分:2)

在给出任何解决方案之前,它似乎与理解角度分量场景有关。我从你的例子中看到了在隔离范围内加载的模板DOM。重复隔离范围

所以,

  

根据组件https://docs.angularjs.org/guide/component

的官方文档[1.5.6]

何时不使用组件:

  • 用于依赖DOM操作,添加事件侦听器等的指令 ,因为编译和链接功能不可用
  • 当您需要高级指令定义选项时,例如priority,terminal,multi-element
  • 当您需要由属性或CSS类而不是元素
  • 触发的指令时
  

如果您查看&#34;指令定义与组件定义之间的比较&#34;从文档中,您将看到

  • 功能------------指令-----------组件
  • 绑定------------否-------------------是(绑定到控制器)
  • bindToController ----是(默认值:false) - 否(使用绑定代替)
  • 编译功能----是------------------否
  • controller ---------- Yes ------------------ Yes(default function(){})
  • controllerAs --------是------------------(默认值:false)是(默认值:$ ctrl)
  • 链接功能------是------------------否

现在

  

组件永远不应该修改任何数据或DOM   拥有范围。通常,在Angular中,可以在任何地方修改数据   在应用程序中通过范围继承和监视。这是   实用,但也可能导致问题,当不清楚哪个   应用程序的一部分负责修改数据。那是   为什么组件指令使用 isolate 范围,所以整个类   范围操纵是不可能的。 参考 ng-doc 1.5.6

但是当你看到,有广告$ postLink()钩子可用,你可能想知道原因是什么?

但是,如果你仔细阅读这个钩子的描述,你会发现......

  

注意包含 templateUrl 指令的子元素不会被编译链接,因为他们正在等待他们的模板异步加载,他们自己的编译和链接已暂停,直到发生。

请参阅下面的Steven Lambert答案......