【问题标题】:knockout.js augment context for each render in template bindingknockout.js 为模板绑定中的每个渲染增加上下文
【发布时间】:2014-08-03 17:33:26
【问题描述】:

我想做什么,我认为需要您在 foreach 或模板绑定中增加每个呈现项目的单独 bindingContexts。

可以通过实例化自定义模板引擎来扩展renderTemplateSource 中的bindingContext 来完成。但这似乎很老套。一定有更好的办法。

什么是更好的方法?这是我的首要问题。

我还想在 foreach 绑定的匿名模板中自动添加一些绑定。这也是,我可以用我的自定义模板引擎来做,但是 afaik 会强制以“数据绑定”属性字符串的形式添加自动绑定。也很时髦。

有没有更简洁的方法让您的自定义绑定潜入其他绑定中,在来自 foreach 的东西上?这是我的第二个问题。

下面是我想要做的更详细的解释。如果你能推荐一些更酷更干净的东西,我谢谢你。


我正在尝试编写一个名为“uiList”的自定义绑定,以便这样使用:

<ul data-bind="uiList: people">
  <li>
    <span data-bind="text: name"></span> 
    ...other stuff ...
  </li>
<ul>

基本上,“foreach”的别名。但我也想要一些典型的列表小部件交互性,例如通过单击来“选择”一个项目。并且一次只选择一个项目。

为了详细说明我的例子,我想让人们的名字可以像这样编辑:

<ul data-bind="uiList: people">
  <li>
    <span data-bind="text: name, visible: !$selected()"></span>
    <input type="text" data-bind="value: name, visible: $selected()"> 
  </li>
<ul>

换句话说,与整个列表的 ui 相关但与数据无关的信息(如选择、禁用等)应在绑定上下文中以简洁明了的方式提供,如上面的$selected 属性。

对于完整的功能,我的 html 可能如下所示:

<ul class="ui-list" data-bind="uiList: people">
  <li class="ui-list-item" data-bind="click: function () {$selected(true)}, css: $selected() ? 'ui-state-selected' : ''">
    <span data-bind="text: name, visible: !$selected()"></span>
    <input type="text" data-bind="value: name, visible: $selected()"> 
  </li>
<ul>

...但这是我希望 uiList 自动化的东西。用户只需要像前面的例子那样写。

这是我的自定义绑定处理程序代码的外观:

var ExtendedForeachTemplateEngine = function (contextExtender, bindings) {
    this.allowTemplateRewriting = false;
    this.contextExtender = contextExtender;

    //prep a data-bind="..." string from the given bindings to
    //sneak in on rendered anon templates
    this.bindingStr = [];
    for (var bindingName in bindings) {
        this.bindingStr.push(bindingName + ':' + bindings[bindingName]);
    }
    this.bindingStr = this.bindingStr.join(', ');
};

ExtendedForeachTemplateEngine.prototype = new ko.templateEngine();

ExtendedForeachTemplateEngine.prototype.renderTemplateSource = function (templateSource, bindingContext) {
    //extend the binding context
    this.contextExtender(bindingContext);

    //sneak in bindings rendered anon templates. Only on the first
    //Element node.
    var nodes = templateSource.nodes().cloneNode(true).childNodes;
    for (var i = 0; i < nodes.length; i ++) {
        if (nodes[i].nodeType !== 1) continue;
        var nodeBindings = nodes[i].getAttribute('data-bind');
        if (nodeBindings) { 
            nodeBindings += ', ' + this.bindingStr;
        } else {
            nodeBindings = this.bindingStr; 
        }
        nodes[i].setAttribute('data-bind', nodeBindings);
        break;
    }
    return nodes;
}

/*
Takes data like a regular foreach. also takes a binding context-extender
callback function, and some bindings to add to each rendered template.
*/
ko.bindingHandlers['extendedForeach'] = {
    init: function (el, va, al, vm, bc) {
        var opts = va();
        ko.applyBindingsToNode(el, {
            template: {
                foreach: opts.data,
                templateEngine: new ExtendedForeachTemplateEngine(opts.contextExtender, opts.bindings)
            }
        }, bc);
    }
};

/*

uiList
-----------

Classes:
- adds class ui-list to the bound element
- renders the inner html for each given item.
- inner html gets ui-list-item class

Selection
- click on one of the rendered item and it is selected.
- context property $selected holds selectedness.
- selected items have class ui-state-selected,
- Only one item can be selected at a time.

*/
ko.bindingHandlers['uiList'] = {
    init: function (el, va, al, vm, bc) {
        var items = va();
        console.log(items);
        var selectedItem = ko.observable(null);
        ko.applyBindingsToNode(el, {
            extendedForeach: {
                data: items,
                bindings: {
                    css: "'ui-list-item' + ($selected() ? ' ui-state-selected' : '')",
                    click: "function () { $selected(true); }"
                },
                contextExtender:  function (context) {
                    context.$selected = ko.computed({
                        read: function () {
                            return selectedItem() === context.$index();
                        },
                        write: function (val) {
                            selectedItem(val ? context.$index() : null);
                        }
                    });
                }
            }
        }, bc);
        $(el).addClass('ui-list');
        return {controlsDescendantBindings: true};
    }
};

【问题讨论】:

    标签: knockout.js custom-binding


    【解决方案1】:

    创建可重复使用的视图模型。这是简短的答案。

    更长的是,您需要一种为它注入模板的好方法,否则您需要在使用模型的任何地方都这样做。一种方法是使用像我的绑定约定库这样的库,这样它将根据视图模型自动呈现正确的模板/视图。

    例如,
    https://github.com/AndersMalmgren/Knockout.BindingConventions/wiki/Template-convention

    另一种方法是将可重用视图模型封装在自定义绑定中,并使用自定义模板源为其呈现模板。您可以查看我的组合框作为示例

    https://github.com/AndersMalmgren/Knockout.Combobox/blob/master/src/knockout.combobox.js#L345

    【讨论】:

    • 制作可重用视图模型可能是对的。但是,这改变了我的问题,是否有某种方法可以为每个呈现的项目实例化 ListItemViewModel(与“人”或个人数据分开)。这样的视图模型可以让您访问有关选择的计算 observables 而无需引用父级(就像您的组合框示例所做的那样),我认为这会很优雅。
    • 在您的情况下,如果不对其父项进行轮询,该项目不可能知道其选择状态?
    • 不,你是对的 - 引用需要在那里。我只是希望它可以隐藏在计算的可观察值中。因为一般来说,有问题的状态可能需要引用父母,或者从多个上下文属性中计算出来。如果能够对 bindingHandler 的用户隐藏所有这些,那就太好了,只需说:“需要知道您的列表视图项是否被选中?询问 $selected 属性 - 我们已经处理了逻辑为此!”
    • 但是最好的方式不是将数据对象包装在包装 ViewModel 中,而是让“ListViewModel”为每个数据项创建它们。就像我在组合框中为我的组合框项目所做的那样。 ListViewModel 的用户不需要做任何事情
    • 啊好吧,现在我明白你的意思了。我没有循环遍历“人”并尝试注入东西,而是循环遍历 new ListViewModel(people).items 之类的东西。这很有道理。
    【解决方案2】:

    您无需自定义任何内容即可做到这一点。

    在 viewModel 构造函数中

    this.people = ko.observableArray(...);
    // create holder for selected person
    this.selectedPerson = ko.observable();
    

    在HTML中,(click: $parent.selectedPerson是可能的,因为selectedPerson是一个函数,ko点击时会调用$parent.selectedPerson($data)

    <ul class="ui-list" data-bind="foreach: people">
      <li class="ui-list-item" data-bind="click: $parent.selectedPerson,
        css: { 'ui-state-selected': $parent.selectedPerson() === $data }">
        <span data-bind="text: name, visible: $parent.selectedPerson() !== $data"></span>
        <input type="text" data-bind="value: name, visible: $parent.selectedPerson() === $data"> 
      </li>
    <ul>
    

    如果你想从 HTML 中隐藏那些额外的细节,而不是使用自定义的 bindingHandler,更好的方法是在组件上使用淘汰赛 3.2.0-beta 新特性,它允许你封装内部模型(如 selectedPerson)和HTML 模板在一起。 http://blog.stevensanderson.com/2014/06/11/architecting-large-single-page-applications-with-knockout-js/

    【讨论】:

    • 当然我可以将状态保留在视图模型中。但重点是编写一些可重用的东西,它封装了“在列表中选择某些东西”的逻辑,而不关心列表项的外观或它绑定的数据。组件功能看起来很有趣。
    • 仔细考虑一下,我认为您以某种方式将数据放入视图模型是正确的。而不是试图扩展上下文。毕竟,这是关于视图的数据。会赞成建议组件,但我没有代表。
    猜你喜欢
    • 2016-12-12
    • 1970-01-01
    • 1970-01-01
    • 2014-06-08
    • 2013-01-17
    • 2012-03-07
    • 2011-07-14
    • 2015-10-05
    • 1970-01-01
    相关资源
    最近更新 更多