3

根据此处给出的建议以及此处给出的有关如何为 forEach 制作自定义 bindingHandler 的信息,我决定尝试为 forEach 和 Masonry 编写自己的自定义绑定。

因为元素是动态添加的,所以不会发生重绘和移动元素以填充空间。所以,这个功能需要在元素被渲染之后移动或者在每个项目被添加之后被调用。

这是我的绑定处理程序

ko.bindingHandlers.masonry = {
init: function (element, valueAccessor, allBindingsAccessor) {
    var $element = $(element),
        originalContent = $element.html();
    $element.data("original-content", originalContent);
    //var msnry = new Masonry($element);
    return { controlsDescendantBindings: true }

},
update: function (element, valueAccessor, allBindingsAccessor) {
    var value = ko.utils.unwrapObservable(valueAccessor()),

    //get the list of items
    items = value.items(),

    //get a jQuery reference to the element
    $element = $(element),

    //get the current content of the element
    elementContent = $element.data("original-content");

    $element.html("");

    var container = $element[0];
    var msnry = new Masonry(container);

    for (var index = 0; index < items.length; index++) {
        (function () {
            //get the list of items
            var item = ko.utils.unwrapObservable(items[index]),
                $childElement = $(elementContent);

            ko.applyBindings(item, $childElement[0]);

            //add the child to the parent        
            $element.append($childElement);
            msnry.appended($childElement[0]);

        })();

        msnry.layout();
        msnry.bindResize();
    }
}

};

以及实现处理程序的 HTML。

<div id="criteriaContainer" data-bind="masonry: { items: SearchItems.Items }">
    <div class="searchCriterion control-group">
        <label class="control-label" data-bind="text: Description"></label>
        <div class="controls">
            <input type="hidden" data-bind="value: Value, select2: { minimumInputLength: 3, queryUri: SearchUri(), placeholder: Placeholder(), allowClear: true }" style="width: 450px">
        </div>
        <p data-bind="text: Value"></p>
    </div>
</div>

当它显示在页面上时,如果通过 append 方法呈现的元素相互叠加,则它会堆叠所有元素。

您可以在我的 bindingHandler 中看到我正在调用 bindResize 以及 layout(),它们似乎都没有任何效果。

这是它在 UI 中的样子的屏幕截图。 Knockout 的砌体示例

4

1 回答 1

2

我制作的自定义绑定器是基于别人对同位素的自定义绑定: https ://github.com/aknuds1/knockout-isotope/blob/master/lib/knockout-isotope.js

注意:自定义同位素绑定的作者使用的是修改版的淘汰赛。下面的绑定使用标准的敲除库(我使用的是 v3.3.0)。

使自定义绑定起作用的技巧是使用 afterAdd 回调来跟踪添加的元素,以便您可以将它们附加到 masonry 对象。

"use strict";

(function () {
    var $container, haveInitialized, newNodes = [], itemClass, masonryOptions;

    function afterAdd(node, index, item) {
        if (node.nodeType !== 1) {
            return; // This isn't an element node, nevermind
        }
        newNodes.push(node);
    }

    ko.bindingHandlers.masonry = {
        defaultItemClass: 'grid-item',
        // Wrap value accessor with options to the template binding,
        // which implements the foreach logic
        makeTemplateValueAccessor: function (valueAccessor) {
            return function () {
                var modelValue = valueAccessor(),
                    options,
                    unwrappedValue = ko.utils.peekObservable(modelValue);    // Unwrap without setting a dependency here

                options = {
                    afterAdd: afterAdd
                };

                // If unwrappedValue.data is the array, preserve all relevant
                // options and unwrap value so we get updates
                ko.utils.unwrapObservable(modelValue);
                ko.utils.extend(options, {
                    'foreach': unwrappedValue.data,
                    'as': unwrappedValue.as,
                    'includeDestroyed': unwrappedValue.includeDestroyed,
                    'templateEngine': ko.nativeTemplateEngine.instance
                });
                return options;
            };
        },
        'init': function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
            console.log({ msg: 'Initializing binding' });

            itemClass = ko.bindingHandlers.masonry.defaultItemClass;
            masonryOptions = {};
            haveInitialized = false;
            $container = $(element);

            var parameters = ko.utils.unwrapObservable(valueAccessor());
            if (parameters && typeof parameters == 'object' && !('length' in parameters)) {
                if (parameters.masonryOptions) {
                    var clientOptions;
                    if (typeof parameters.masonryOptions === 'function') {
                        clientOptions = parameters.masonryOptions();
                        if (typeof clientOptions !== 'object') {
                            throw new Error('masonryOptions callback must return object');
                        }
                    } else if (typeof parameters.masonryOptions !== 'object') {
                        throw new Error('masonryOptions must be an object or function');
                    } else {
                        clientOptions = parameters.masonryOptions;
                    }
                    ko.utils.extend(masonryOptions, clientOptions);
                }
                if (parameters.itemClass) {
                    itemClass = parameters.itemClass;
                }
            }

            // Initialize template engine, moving child template element to an
            // "anonymous template" associated with the element
            ko.bindingHandlers.template.init(
                element,
                ko.bindingHandlers.masonry.makeTemplateValueAccessor(valueAccessor)
            );

            return { controlsDescendantBindings: true };
        },
        'update': function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
            ko.bindingHandlers.template.update(element,
                    ko.bindingHandlers.masonry.makeTemplateValueAccessor(valueAccessor),
                    allBindingsAccessor, viewModel, bindingContext);

            // Make this function depend on the view model, so it gets called for updates
            var data = ko.bindingHandlers.masonry.makeTemplateValueAccessor(
                        valueAccessor)().foreach;
            ko.utils.unwrapObservable(data);

            if (!haveInitialized) {
                masonryOptions.itemSelector = '.' + itemClass;
                console.log({msg: 'Binding update called for 1st time, initializing Masonry', options: masonryOptions});
                $container.masonry(masonryOptions);
            }
            else {
                console.log({ msg: 'Binding update called again, appending to Masonry', elements: newNodes });
                var newElements = $(newNodes);
                $container.masonry('appended', newElements);
                $container.masonry('layout');
                newNodes.splice(0, newNodes.length); // reset back to empty
            }

            // Update gets called upon initial rendering as well
            haveInitialized = true;
            return { controlsDescendantBindings: true };
        }
    };
})();

以下是使用中的绑定示例:

<div class="grid" data-bind="masonry: {data: blogEntries, masonryOptions: { itemClass: 'grid-item', columnWidth: 320, gutter: 10}}">
    <div class="grid-item">
        <div data-bind="css: {'idea-blog': isIdea }">
            <img data-bind="attr: { src: imageUrl }">
            <h2 data-bind="text: title"></h2>
            <p data-bind="text: description"></p>
            <div class="button-keep-reading">
                <a data-bind="attr: { src: articleUrl }"><span data-bind="text: linkText"></span> &gt;</a>
            </div>
        </div>
    </div>
</div>

请注意,您需要确保在绑定数据之前加载您在砌体瓷砖中使用的任何图像,否则砌体会出现布局问题。

于 2015-06-19T01:49:37.870 回答