【问题标题】:$postLink of an angular component/directive running too early角度组件/指令的 $postLink 过早运行
【发布时间】:2016-09-05 21:56:18
【问题描述】:

更新:我悬赏这个问题。我不是在寻找黑客或解决方法。我正在寻找一种官方方式来访问角度组件中的 dom,并解释为什么我看到的行为($postLink 运行到早期)似乎与官方文档相矛盾。
官方文档状态 (here):

$postLink() - 在这个控制器的元素和它的子元素被链接之后调用。与 post-link 函数类似,该钩子可用于设置 DOM 事件处理程序并进行直接 DOM 操作

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

我正在使用一个角度组件,我想在post链接函数中修改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 对象是没有用的。

【问题讨论】:

标签: javascript angularjs


【解决方案1】:

在给出任何解决方案之前,它似乎与理解角度组件场景有关。我从您的示例中看到的模板 DOM 加载在 isolated 范围内。重复一个孤立的范围

所以,

根据官方文档 [1.5.6] 关于组件https://docs.angularjs.org/guide/component

何时不使用组件:

  • 对于依赖 DOM 操作的指令,添加事件监听器等, 因为编译和链接功能不可用
  • 当您需要优先级、终端、多元素等高级指令定义选项时
  • 当您需要由属性或 CSS 类而不是元素触发的指令时

如果您查看文档中的“指令定义和组件定义之间的比较”部分,您会看到

  • 功能------------指令-----------组件
  • 绑定------------否-------是(绑定到控制器)
  • bindToController----是(默认:false)-否(改用绑定)
  • 编译函数----是-------否
  • 控制器-----------是-------是(默认函数(){})
  • controllerAs--------Yes------------------(默认:false)是(默认:$ctrl)
  • 链接功能-----是-------否

现在

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

但是当你看到有 ad $postLink() hook 可用时,你可能想知道这是什么原因?

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

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

请参考下面 Steven Lambert 的回答...

【讨论】:

  • 关于最后一条评论,我认为子元素是指当前组件内的其他角度指令。在我的模板中,目前只有基本的 html。如果没有编译和链接templateUrl,为什么他们调用那个回调函数$postLink?
  • 我也在探索并发现 2 个可能的解决方法,1 是使用 $compileProvider 服务,另一个是在 postlink 中编译 templateUrl 并将属性分配给组件。您也可以尝试这种方式或任何其他视角。希望很快就会有解决方案
  • 我把它改成了 template 而不是 templateUrl 还是不行。关于异步加载的 URL 的注释与此问题无关,它仅指子指令
  • 您是否查看了文档中的组件间通信部分?我现在在办公室,我回家再做一些 RND。
  • 我也在寻找答案,这让我很抓狂。这也可以是一种解决方法stackoverflow.com/questions/34745155/…
【解决方案2】:

关于$postLink() 的文档是正确的。在它的控制器元素和它的子元素被链接之后调用它。这并不意味着您会立即看到指令的结果。也许它正在调用$http 并在结果到达后插入结果。也许它正在注册一个观察者,该观察者反过来设置结果,就像大多数 Angular 的内置指令一样

在您的情况下,根本问题是您希望在 编译插值之后执行 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 作为父级,并在那里添加适当的逻辑。这样,每个组件的职责就可以更好地定义,并且更容易测试。

【讨论】:

  • 这是正确的答案,所以我授予了赏金。经过我自己的调查,我或多或少地知道了细节,但这个答案更清楚地解释了它。此外,将 $watch 与函数一起使用而不是超时是一个非常好的补充 :) 我将尝试使用子组件来实现它,看看是否可行...
  • 已经有一段时间了,但是您介意提供更多详细信息、外部链接或您提到的 Angualr 方式的示例,以避免使用 $timeout 吗?非常感谢!
【解决方案3】:

误解在于角度 $digest 循环如何与控制器生命周期事件(和指令生命周期事件)结合使用。

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

post 链接函数在所有内容被链接后运行,但它是 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
        });
    }
}

【讨论】:

  • 谢谢你说清楚,我正在寻找这样正确的提示。
猜你喜欢
  • 2017-04-01
  • 2017-05-24
  • 2017-09-22
  • 2015-05-03
  • 1970-01-01
  • 2016-10-01
  • 1970-01-01
  • 1970-01-01
  • 2019-03-01
相关资源
最近更新 更多