【问题标题】:With KnockoutJS, how can I scroll to a component after it's rendered in a foreach?使用 KnockoutJS,如何在 foreach 中呈现组件后滚动到该组件?
【发布时间】:2018-01-17 14:52:03
【问题描述】:

我启用了deferred updates

我有两个组件。

第一个是列表,简单实现为带有foreach数据绑定的div:

<div class="list-people" data-bind="foreach: { data: people, afterRender: afterRenderPeople }">
    <!-- ko component: { name: "listitem-person", params: { person: $data } } --><!-- /ko -->
</div>

第二个是列表项:

<div class="listitem-person">
    <span data-bind="text: Name"></span>
</div>

foreach 中的每个项目调用afterRender

我的afterRenderPerson 函数很简单:

public afterRenderPerson = (elements: any[], data: Person) => {
    let top = $(element[0]).offset().top;

    scrollTo(top);
};

问题是当afterRenderPerson被调用时,子组件listitem-person还没有被渲染出来。

也就是说传递给afterRenderPerson的元素数组有4个节点:

  1. 包含\n 的文本节点,即新行。
  2. 包含&lt;!-- ko component: { name: "listitem-person", params: { person: $data } } --&gt;的评论节点。
  3. 包含&lt;!-- /ko --&gt;的评论节点。
  4. 包含\n 的文本节点,即一个新行。

这些都不适合获取top 像素,即使它们适合,正在渲染的子组件也可能会影响该位置的布局,从而改变我试图获取的像素的值。

【问题讨论】:

  • 我能想到的唯一解决方法是定义一个自定义的scrollTo 绑定并将其包含在组件模板中... 很容易实现,但仍然感觉很hacky并且使您的内部组件更难重用.您可能还想跟踪这个feature request
  • 我一直在寻找的解决方法是使用 setInterval 来检测元素何时可通过 ID 获得。但是不得不诉诸这种方式让我觉得很肮脏。

标签: javascript typescript knockout.js


【解决方案1】:

不幸的是,documentation for foreach 似乎没有考虑到组件的延迟特性。

如果您需要在生成的 DOM 元素上运行一些进一步的自定义逻辑,您可以使用下面描述的任何 afterRender/afterAdd/beforeRemove/beforeMove/afterMove 回调。

注意:这些回调仅用于触发与列表更改相关的动画。

我遇到了两种解决方法,但都不是很好,但这就是为什么它们是解决方法而不是解决方案!

user3297291 在评论中提出了将scrollTo 绑定放置在子组件上的建议。

我能想到的唯一解决方法是定义一个自定义的 scrollTo 绑定并将其包含在组件模板中......很容易实现,但仍然感觉很hacky并且使您的内部组件更难重用。您可能还想跟踪这个feature request – user3297291

这只是一个custom binding,它根据提供给它的值有条件地执行一些代码。

在将 HTML 插入 DOM 之前,不会调用绑定。这并不完美,因为以后对 DOM 的更改可能会影响插入的 HTML 元素的位置,但它应该适用于许多情况。

虽然我不是很热衷于修改子组件,但我更喜欢封装在父组件中的解决方案。

第二种解决方法是通过其 ID 检查子组件 HTML 元素是否存在于 DOM 中。因为我不知道它们什么时候会出现,所以必须在某种循环中完成。

while 循环不适合,因为它会在“紧密”循环中过于频繁地运行检查,因此使用 setTimeout

setTimeout 是一个可怕的 hack,使用它让我觉得很脏,但它确实适用于这种情况。

private _scrollToOffset = -100;
private _detectScrollToDelayInMS = 200;
private _detectScrollToCountMax = 40;
private _detectScrollToCount = 0;

private _detectScrollTo = (scrollToContainerSelector: string, scrollToChildSelector: string) => {
    //AJ: If we've tried too many times then give up.
    if (this._detectScrollToCount >= this._detectScrollToCountMax)
        return;

    setTimeout(() => {
        let foundElements = $(scrollToChildSelector);

        if (foundElements.length > 0) {
            //AJ: Scroll to it
            $(scrollToContainerSelector).animate({ scrollTop: foundElements.offset().top + this._scrollToOffset });

            //AJ: Give it a highlight
            foundElements.addClass("highlight");
        } else {
            //AJ: Try again
            this._detectScrollTo(scrollToContainerSelector, scrollToChildSelector);
        }
    }, this._detectScrollToDelayInMS);

    this._detectScrollToCount++;
};

我确保限制了它可以运行多长时间,所以如果出现问题,它不会永远循环。

可能应该指出,对于这个问题有一个“终极”解决方案,那就是 TKO,AKA Knockout 4。

但这还不是“生产就绪”。

How to know when a component has finished updating DOM?

brianmhunt 于 6 月 20 日发表评论

knockout/tko(ko 4 候选)最新的master分支有这个。

更具体地说,applyBindings 系列函数现在返回一个 Promise,该 Promise 在子子项(包括异步子项)被绑定时解析。

API 尚未设置或记录,但骨骼已设置。

【讨论】:

  • 很好的研究,@AndyJ,感谢您深入挖掘并发布此内容。仅供参考,尽管它处于 alpha 阶段,但现在有一些网站在生产中使用 TKO/KO4 来开发极其复杂的应用程序。需要注意的是,这些网站仅限于现代浏览器。 TKO 还没有经过 IE/Edge/Safari/IOS/Opera 的充分测试。
  • @BrianM.Hunt 很高兴知道谢谢。我们仍然坚持使用 IE11,虽然它仍然得到官方支持,但我们会在它死去的那一天独身!我认为 Edge 是一种现代浏览器,尽管它的使用肯定不是很广泛。当我们继续进行下一个项目时,我将不得不评估 TKO 并看看它是否合适。
  • setTimeout 是否需要多次迭代?
  • @RoyJ 好问题,我目前没有任何登录记录,因此没有数据显示我的具体情况的一种或另一种方式。使用限制多次执行检查是相当蛮力的,但它应该适用于浏览器可能存在差异、Knockout 更改或组件更改的大多数情况。如果在这上面花费更多时间,我会查看第一个 setTimeout 的持续时间为 0,然后在后续尝试中使用更高的值。我还会调查用于 KO 延迟渲染的knockoutjs.com/documentation/microtasks.html
【解决方案2】:

这似乎有效。我制作了一个绑定处理程序,它在其init 中运行回调(它使用tasks.schedule 来允许渲染周期)。在父级附加它不会及时渲染子级,但将它附加到虚拟元素会。

我将它设计为与签名类似afterRender 的函数一起使用。因为它对每个元素都运行,所以回调函数必须测试数据是否是第一个元素。

ko.options.deferUpdates = true;

ko.bindingHandlers.notify = {
  init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
    // Make it asynchronous, to allow Knockout to render the child component
    ko.tasks.schedule(() => {
      const onMounted = valueAccessor().onMounted;
      const data = valueAccessor().data;
      const elements = [];

      // Collect the real DOM nodes (ones with a tagName)
      for(let child=ko.virtualElements.firstChild(element);
          child;
          child=ko.virtualElements.nextSibling(child)) {
        if (child.tagName) { elements.push(child); }
      }
      onMounted(elements, data);
    });
  }
};

ko.virtualElements.allowedBindings.notify = true;

function ParentVM(params) {
  this.people = params.people;
  this.afterRenderPeople = (elements, data) => {
    console.log("Elements:", elements.map(e => e.tagName));
    if (data === this.people[0]) {
      console.log("Scroll to", elements[0].outerHTML);
      //let top = $(element[0]).offset().top;

      //scrollTo(top);
    }
  };
}

ko.components.register('parent-component', {
  viewModel: ParentVM,
  template: {
    element: 'parent-template'
  }
});

function ChildVM(params) {
  this.Name = params.person;
}

ko.components.register('listitem-person', {
  viewModel: ChildVM,
  template: {
    element: 'child-template'
  }
});

vm = {
  names: ['One', 'Two', 'Three']
};

ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>

<template id="parent-template">
  <div class="list-people" data-bind="foreach: people">
    <!-- ko component: { name: "listitem-person", params: { person: $data } }, notify: {onMounted: $parent.afterRenderPeople, data: $data} -->
    <!-- /ko -->
  </div>
</template>

<template id="child-template">
  <div class="listitem-person">
    <span data-bind="text: Name"></span>
  </div>
</template>

<parent-component params="{ people: names }">
</parent-component>

【讨论】:

  • 感谢您的建议,如果在渲染子组件之后确实调用了 afterRenderPerson,那将是一个不错的选择。问题是 afterRenderPerson 被调用 before 子组件完成渲染。 “afterRender”绑定可能应该称为“afterHtmlInserted”。
  • 组件没有 afterRender 绑定:github.com/knockout/knockout/issues/1533 并且 foreach 的文档明确指出 foreach afterRender 应该是做动画的地方 knockoutjs.com/documentation/foreach-binding.html 不幸的是,foreach 文档似乎没有' t 考虑组件的延迟性质。您可以制作一种“假” afterRender 绑定,这就是 user3297291 在他的评论中所建议的。
  • @AndyJ 我用你提到的一些想法对此进行了另一次尝试。
  • 我已经添加到绑定处理程序中,目前 init 没有被调用。我很确定这是我的问题,所以我会调查并尝试调试正在发生的事情。现在已经到周五工作日结束了,所以我会在周一早上把这个拿回来。但我想对你在这个问题上付出的努力表示感谢。你的坚持令人钦佩,我希望我能让这段代码正常工作,这样我就能接受你的回答。顺便说一句,我喜欢你的个人资料照片!它的灵感来自于 Bugs 吗? images-na.ssl-images-amazon.com/images/I/…
  • @AndyJ 是的,我的头像是受到 Bugs 的启发。许多年前,我和一个朋友在大学里为“What's Opera Doc”进行了“空中乐队”表演。那是我戴的帽子。
猜你喜欢
  • 1970-01-01
  • 2019-05-14
  • 2018-03-26
  • 2019-01-27
  • 1970-01-01
  • 1970-01-01
  • 2021-04-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多