KNOCKOUTJS DOCUMENTATION-绑定(BINDINGS)-自定义绑定

除了ko内建的绑定,还可以自定义绑定,灵活地封装复杂的行为使之可重用。

自定义绑定

注册自定义绑定

向 ko.bindingHandles
添加一个子属性来注册一个绑定。

ko.bindingHandlers.yourBindingName = {
    init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
        // This will be called when the binding is first applied to an element
        // Set up any initial state, event handlers, etc. here
    },
    update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
        // This will be called once when the binding is first applied to an element,
        // and again whenever any observables/computeds that are accessed change
        // Update the DOM element based on the supplied values here.
    }
};

然后就可以向DOM元素使用它了。

<div data-bind="yourBindingName: someValue"> </div>

回调函数

创建自定义绑定时需要提供回调函数 init
和 update
,可以只提供其中一个。

ko会在绑定应用到元素上时调用 init
,之后便不再调用,一般用于初始化元素状态和注册事件的回调函数。

ko会在初始绑定应用到元素上时调用 update
,然后在其访问的任意依赖变化时也会调用。

回调函数的参数:

  • element:使用绑定的DOM元素。
  • valueAccessor:返回绑定的模型属性的js函数。例如调用 valueAccessor()
    来获取当前模型属性值。为了能兼容监控变量和普通值,使用 ko.unwrap
    ,如 ko.unwrap(valueAccessor())
  • allBindings:可以用来访问DOM元素绑定的所有模型值的js对象。例如 allBindings.get(“name”)
    获取 name
    绑定的值,不存在时为 undefined
    。可以用 allBindings.has(“name”)
    来检测其是否存在。
  • bindingContext:包含该元素的绑定可用的上下文的对象。(在v3版本之前,该参数是绑定的试图模型 viewModel
    ,现在可用 bindingContext.$data
    或 bindingContext.$rawData
    来代替。)

例如,可以用 visible
绑定来控制元素的可见性,但希望在元素状态变化时添加动画。可以使用jQuery的 slideUp
/ slideDown
创建一个自定义绑定。

ko.bindingHandlers.slideVisible = {
    update: function(element, valueAccessor, allBindings) {
    // First get the latest data that we"re bound to
    var value = valueAccessor();
    // Next, whether or not the supplied model property is observable, get its current value
    var valueUnwrapped = ko.unwrap(value);
    // Grab some more data from another binding property
    var duration = allBindings.get("slideDuration") || 400; // 400ms is default duration unless otherwise specified
    // Now manipulate the DOM element
    if (valueUnwrapped == true)
        $(element).slideDown(duration); // Make the element visible
    else
        $(element).slideUp(duration);   // Make the element invisible
    }
};
<div data-bind="slideVisible: giftWrap, slideDuration:600">You have selected the option</div>
<label><input type="checkbox" data-bind="checked: giftWrap" /> Gift wrap</label>
 
<script type="text/javascript">
    var viewModel = {
        giftWrap: ko.observable(true)
    };
    ko.applyBindings(viewModel);
</script>

如果希望 slideVisible
在页面第一次加载时不使用动画,只是在用户改变模型状态时才出现动画,可以添加 init
方法:

ko.bindingHandlers.slideVisible = {
    init: function(element, valueAccessor) {
        var value = ko.unwrap(valueAccessor()); // Get the current value of the current property we"re bound to
        $(element).toggle(value); // jQuery will hide/show the element depending on whether "value" or true or false
    },
    update: function(element, valueAccessor, allBindings) {
        // Leave as before
    }
};

DOM变化后更新监控属性

上面展示了如何使用 update
来根据监控属性变化更新DOM元素,那怎样在用户操作DOM元素之后更新关联的监控属性呢?

可以在 init
中注册一些事件的回调函数来更新监控属性。

ko.bindingHandlers.hasFocus = {
    init: function(element, valueAccessor) {
        $(element).focus(function() {
            var value = valueAccessor();
            value(true);
        });
        $(element).blur(function() {
            var value = valueAccessor();
            value(false);
        });
    },
    update: function(element, valueAccessor) {
        var value = valueAccessor();
        if (ko.unwrap(value))
            element.focus();
        else
            element.blur();
    }
};
<p>Name: <input data-bind="hasFocus: editingName" /></p>
 
<!-- Showing that we can both read and write the focus state -->
<div data-bind="visible: editingName">You"re editing the name</div>
<button data-bind="enable: !editingName(), click:function() { editingName(true) }">Edit name</button>
 
<script type="text/javascript">
    var viewModel = {
        editingName: ko.observable()
    };
    ko.applyBindings(viewModel);
</script>

控制后代的自定义绑定

默认情况下,绑定只会作用于应用它的元素。如果希望作用于所有后代元素,只需在 init
中 return { controlsDescendantBindings: true }

ko.bindingHandlers.allowBindings = {
    init: function(elem, valueAccessor) {
        // Let bindings proceed as normal *only if* my value is false
        var shouldAllowBindings = ko.unwrap(valueAccessor());
        return { controlsDescendantBindings: !shouldAllowBindings };
    }
};
<div data-bind="allowBindings: true">
    <!-- This will display Replacement, because bindings are applied -->
    <div data-bind="text: "Replacement"">Original</div>
</div>
<div data-bind="allowBindings: false">
    <!-- This will display Original, because bindings are not applied -->
    <div data-bind="text: "Replacement"">Original</div>
</div>

向后代绑定传递额外参数

通常使用 controlsDescendantBindings
的绑定也会调用 ko.applyBindingsToDescendants(someBindingContext, element)
,使后代绑定应用于一些修改的上下文。

例如,自定义的绑定 withProperties
向绑定上下文附加一些额外的属性,以便在后代绑定中能访问。

ko.bindingHandlers.withProperties = {
    init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
        // Make a modified binding context, with a extra properties, and apply it to descendant elements
        var innerBindingContext = bindingContext.extend(valueAccessor);
        ko.applyBindingsToDescendants(innerBindingContext, element);
        // Also tell KO *not* to bind the descendants itself, otherwise they will be bound twice
        return { controlsDescendantBindings: true };
    }
};

可以看到,绑定上下文有个 extend
方法,返回一个复制的并带有额外属性的上下文。 extend
方法接受的参数要么是一个带有属性的对象,要么是能返回这样的对象的函数。建议使用函数作为参数,这样后续的绑定值变化也会更新到绑定上下文中,并且不影响当前的绑定上下文,也就是说不影响兄弟层级的元素,只会影响后代元素。

<div data-bind="withProperties: { emotion: "happy" }">
    Today I feel <span data-bind="text: emotion"></span>. <!-- Displays: happy -->
</div>
<div data-bind="withProperties: { emotion: "whimsical" }">
    Today I feel <span data-bind="text: emotion"></span>. <!-- Displays: whimsical -->
</div>

在绑定上下文中添加额外的层级

with
和 foreach
绑定会在绑定上下文中添加额外的层级。这意味着它们的后代能够使用 $parent
、 $parents
、 $root
或 $parentContext
来访问外层数据。

如果在自定义绑定中实现该功能,除了使用 bindingContext.extend()
,还可以使用 bindingContext.createChildContext(someData)
。它会返回一个新上下文,该上下文的视图模型是 someData
,父上下文 $parentContext
是 bindingContext
。如果需要,还可以使用 ko.utils.extend
为该子上下文扩展额外的属性。

ko.bindingHandlers.withProperties = {
    init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
        // Make a modified binding context, with a extra properties, and apply it to descendant elements
        var childBindingContext = bindingContext.createChildContext(
            bindingContext.$rawData, 
            null, // Optionally, pass a string here as an alias for the data item in descendant contexts
            function(context) {
                ko.utils.extend(context, valueAccessor());
            });
        ko.applyBindingsToDescendants(childBindingContext, element);
        // Also tell KO *not* to bind the descendants itself, otherwise they will be bound twice
        return { controlsDescendantBindings: true };
    }
};

这样, withProperties
绑定就可以嵌套使用了,并且每个嵌套层级可以通过 $parentContext
访问父层。

<div data-bind="withProperties: { displayMode: "twoColumn" }">
    The outer display mode is <span data-bind="text: displayMode"></span>.
    <div data-bind="withProperties: { displayMode: "doubleWidth" }">
        The inner display mode is <span data-bind="text: displayMode"></span>, but I haven"t forgotten
        that the outer display mode is <span data-bind="text: $parentContext.displayMode"></span>.
    </div>
</div>

支持虚拟元素的自定义绑定

使用 ko.virtualElements.allowedBindings
告诉ko自定义绑定能够识别虚拟元素。

例如,创建一个对DOM节点随机排序的自定义绑定:

ko.bindingHandlers.randomOrder = {
    init: function(elem, valueAccessor) {
        // Pull out each of the child elements into an array
        var childElems = [];
        while(elem.firstChild)
            childElems.push(elem.removeChild(elem.firstChild));
        // Put them back in a random order
        while(childElems.length) {
            var randomIndex = Math.floor(Math.random() * childElems.length),
                chosenChild = childElems.splice(randomIndex, 1);
            elem.appendChild(chosenChild[0]);
        }
    }
};

这对普通DOM元素是可用的:

<div data-bind="randomOrder: true">
    <div>First</div>
    <div>Second</div>
    <div>Third</div>
</div>

但是对虚拟元素却不起作用,会报错“The binding “randomOrder” cannot be used with virtual elements”:

<!-- ko randomOrder: true -->
    <div>First</div>
    <div>Second</div>
    <div>Third</div>
<!-- /ko -->

要想 randomOrder
支持虚拟元素,需要配置ko:

ko.virtualElements.allowedBindings.randomOrder = true;

这样就不会报错了,但仍然不起作用,因为绑定中调用的是普通DOM的API,不识别虚拟元素。这就是为什么ko要求显式地声明支持虚拟元素。使用虚拟元素API更新自定义绑定:

ko.bindingHandlers.randomOrder = {
    init: function(elem, valueAccessor) {
        // Build an array of child elements
        var child = ko.virtualElements.firstChild(elem),
            childElems = [];
        while (child) {
            childElems.push(child);
            child = ko.virtualElements.nextSibling(child);
        }
        // Remove them all, then put them back in a random order
        ko.virtualElements.emptyNode(elem);
        while(childElems.length) {
            var randomIndex = Math.floor(Math.random() * childElems.length),
                chosenChild = childElems.splice(randomIndex, 1);
            ko.virtualElements.prepend(elem, chosenChild[0]);
        }
    }
};

现在该绑定对普通DOM元素和虚拟元素都支持了,因为 ko.virtualElements
的API是兼容普通DOM元素的。

虚拟元素API

  • ko.virtualElements.allowedBindings

    该对象的key决定哪些绑定可用于虚拟元素。如 ko.virtualElements.allowedBindings.mySuperBinding = true
    意味着允许 mySuperBinding
    用于虚拟元素。

  • ko.virtualElements.emptyNode(containerElem)

移除真实或虚拟元素 containerElem
的所有子节点(通过清除他们关联的任何数据来避免内存泄露)。

  • ko.virtualElements.firstChild(containerElem)

返回真实或虚拟元素 containerElem
的第一个子节点,不存在时返回null。

  • ko.virtualElements.insertAfter(containerElem, nodeToInsert, insertAfter)

添加 nodeToInsert
为真实或虚拟元素 containerElem
的子节点,位置在 insertAfter
之后( insertAfter
必须是 containerElem
的子节点)。

  • ko.virtualElements.nextSibling(node)

返回真实或虚拟元素的 node
的后的兄弟节点,不存在时返回null。

  • ko.virtualElements.prepend(containerElem, nodeToPrepend)

将 nodeToPrepend
添加为真实或虚拟元素 containerElem
的第一个子节点。

  • ko.virtualElements.setDomNodeChildren(containerElem, arrayOfNodes)

移除真实或虚拟元素 containerElem
的所有子节点(通过清除他们关联的任何数据来避免内存泄露),并将 arrayOfNodes
中的节点作为的新的子节点插入。

自定义DISPOSE

在使用模板绑定或流程控制绑定时,一些DOM元素会被动态添加或移除。当创建自定义绑定时,通常需要添加ko移除关联的元素时的清除逻辑。

为元素的释放注册回调函数

调用 ko.utils.domNodeDisposal.addDisposeCallback(node, callback)
来注册节点被删除的回调函数。

ko.bindingHandlers.myWidget = {
    init: function(element, valueAccessor) {
        var options = ko.unwrap(valueAccessor()),
            $el = $(element);
        $el.myWidget(options);
        ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
            // This will be called when the element is removed by Knockout or
            // if some other part of your code calls ko.removeNode(element)
            $el.myWidget("destroy");
        });
    }
};

覆盖外部数据的清除逻辑

当ko移除元素时会执行清除逻辑来清除该元素关联的任何数据。在清除逻辑中,如果jQuery可用,那么是通过调用jQuery的 cleanData
方法。在一些高级的应用场景中,可能希望阻止或自定义数据移除的方式。可以通过覆盖 ko.utils.domNodeDisposal.cleanExternalData(node)
支持自定义逻辑。例如,要阻止调用 cleanData
,可以用一个空函数来替代标准的 cleanExternalData
实现。

ko.utils.domNodeDisposal.cleanExternalData = function () {
    // Do nothing. Now any jQuery data associated with elements will
    // not be cleaned up when the elements are removed from the DOM.
};

使用预处理(PREPROCESSING)扩展KO的绑定语法

从ko V3.0版本开始,ko支持通过添加一个可以在绑定过程中重写DOM节点并绑定字符串的回调函数来定义一个自定义语法。

预处理绑定字符串

可以为某个绑定处理器(click、visible或自定义绑定)提供一个绑定预处理器(binding preprocessor)来链接到ko的 data-bind
解释逻辑中。

通过对绑定处理器添加 preprocess
函数。

ko.bindingHandlers.yourBindingHandler.preprocess = function(stringFromMarkup) {
    // Return stringFromMarkup if you don"t want to change anything, or return
    // some other string if you want Knockout to behave as if that was the
    // syntax provided in the original HTML
}

例1:为绑定设置默认值

如果绑定是没有指定value,会默认为 undefined
。可以使用预处理器使绑定有个初始值。

ko.bindingHandlers.uniqueName.preprocess = function(val) {
    return val || "true";
}

uniqueName
将允许绑定时不指定value,而默认为 true

<input data-bind="value: someModelProperty, uniqueName" />

例2:为事件绑定表达式

如果希望为 click
事件绑定表达式(而不是ko期望的函数引用),可以使用预处理器。

ko.bindingHandlers.click.preprocess = function(val) {
    return "function($data,$event){ " + val + " }";
}
<button type="button" data-bind="click: myCount(myCount()+1)">Increment</button>

绑定预处理器

ko.bindingHandlers. .preprocess(value, name, addBindingCallback)

如果定义了该函数,那么在绑定被评估之前每个 <name>
的绑定时都会调用该函数。

preprocess
的参数如下:

  • value:ko试图解析绑定之前,绑定的值的语法。(例如 yourBinding: 1 + 1
    中,关联的值是字符串"1 + 1")
  • name:绑定的名称。(例如 yourBinding: 1 + 1
    中,名称是字符串"yourBinding")
  • addBinding:可选的回调函数,用于为当前元素添加另一个绑定。它有两个参数, name
    和 value
    。例如,在预处理方法中,调用 addBinding(“visible”, “acceptsTerms()”);
    来让ko表现得看起来就像元素已经有了 visible: acceptsTerms()
    绑定。

preprocess
必须返回一个新的字符串值来被解析并传递给绑定,或者返回 undefined
来移除绑定。返回非字符串值是没有意义的,因为HTML一定是字符串。例如将”value + ".toUpperCase()"”作为字符串返回,那么 yourBinding: "Bert"
会被解析成就像HTML中包含 yourBinding: "Bert".toUpperCase()
一样。ko会按照正常方式解析返回的值,因此必须是合法的js表达式。

预处理DOM节点

可以提供一个节点预处理器(node preprocessor)来链接到ko的DOM处理中。ko会在遍历每个DOM节点时调用该函数一次,包括初次绑定UI,和后续的任意DOM子树被注入时。

通过向 bindingProvider
提供一个 preprocessNode

ko.bindingProvider.instance.preprocessNode = function(node) {
    // Use DOM APIs such as setAttribute to modify "node" if you wish.
    // If you want to leave "node" in the DOM, return null or have no "return" statement.
    // If you want to replace "node" with some other set of nodes,
    //  - Use DOM APIs such as insertChild to inject the new nodes
    //    immediately before "node"
    //  - Use DOM APIs such as removeChild to remove "node" if required
    //  - Return an array of any new nodes that you"ve just inserted
    //    so that Knockout can apply any bindings to them
}

例3:虚拟模板元素

用常规的语法在虚拟元素上使用模板时比较复杂。通过使用预处理,可以添加一个使用单注解的模板格式:

ko.bindingProvider.instance.preprocessNode = function(node) {
    // Only react if this is a comment node of the form <!-- template: ... -->
    if (node.nodeType == 8) {
        var match = node.nodeValue.match(/^s*(templates*:[sS]+)/);
        if (match) {
            // Create a pair of comments to replace the single comment
            var c1 = document.createComment("ko " + match[1]),
                c2 = document.createComment("/ko");
            node.parentNode.insertBefore(c1, node);
            node.parentNode.replaceChild(c2, node);
            // Tell Knockout about the new nodes so that it can apply bindings to them
            return [c1, c2];
        }
    }
}

然后可以在视图中这样使用模板:

<!-- template: "some-template" -->

KO.BINDINGPROVIDER.INSTANCE.PREPROCESSNODE(NODE)

在绑定处理前每个DOM节点都会调用该函数。它可以修改、移除或替换节点 node
。任何新节点回被立刻插入到 node
之前,并且如果有节点被添加或 node
被移除,该函数必须返回html文档中 node
位置的新的节点数组。

参考资料:

KnockoutJS Documentation

上一篇:java poi 从服务器下载模板写入数据再导出


下一篇:WCF - 自定义绑定