使用 KnockoutJS,如何在组件在 foreach 中渲染后滚动到组件?



我启用了延迟更新。

我有两个组件。

第一个是列表,它简单地实现为带有 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>

afterRenderforeach中的每个项目调用。

我的afterRenderPerson函数非常简单:

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

问题是,当afterRenderPerson被称为子组件时,listitem-person尚未呈现。

这意味着传递给afterRenderPerson的元素数组有 4 个节点:

  1. 包含n的文本节点,即换行符。
  2. 包含<!-- ko component: { name: "listitem-person", params: { person: $data } } -->的注释节点。
  3. 包含<!-- /ko -->的注释节点。
  4. 包含n的文本节点,即换行符。

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

不幸的是,foreach 的文档似乎没有考虑到组件的延迟性质。

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

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

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

User3297291 在注释中给出了创建放置在子组件上的scrollTo绑定的建议。

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

这只是一个自定义绑定,它根据提供给它的值有条件地执行某些代码。

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

不过,我不太热衷于修改子组件,当保留在父组件中时,我更喜欢解决方案。

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

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

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

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,又名淘汰赛4。

但这还不是"生产就绪"。

如何知道组件何时完成 DOM 更新?

布莱恩亨特在6月20日评论

淘汰赛/TKO(KO 4 候选人(最新的主分支有这个。

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

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

这似乎有效。我创建了一个绑定处理程序,它在其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>

最新更新