Custom binding for when foreach has finished rende

2019-07-13 19:42发布

问题:

I have an observable array with items bound to an ul. I need to calculate the width of the entire set of li items once the items have been added so I can set the width of the ul element to the total width of the child li elements.

How can I go about doing this with a foreach binding?

<div>
    <h2 data-bind="visible: items().length">Test</h2>
    <ul data-bind="foreach: { data: items, afterAdd: $root.myAfterAdd }">
        <li data-bind="text: name"></li>
    </ul>
</div>

And the JavaScript:

var viewModel = {
    items: ko.observableArray([
        { name: "one" },
        { name: "two" },
        { name: "three" }
        ]),
    myAfterAdd: function(element) {
        if (element.nodeType === 1) {
           alert("myAfterAdd");   
        }
    },
    addItem: function() {
        this.items.push({ name: 'new' });
    }
};


ko.applyBindings(viewModel);

// once foreach has finished rendering I have to set the width 
// of the ul to be the total width of the children!

I tried using afterAdd so I can 'update' the width of the ul after each element is added, unfortunately I have discovered that afterAdd does not fire for the initial items! It will only fire when pushing additional items...

Note that the content inside the li items will be of unknown width :)

See this fiddle for a sample scenario.

回答1:

I had a similar problem. You can use this to make that initial calculation:

ko.bindingHandlers.mybinding {
        init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
            var foreach = allBindings().foreach,
                subscription = foreach.subscribe(function (newValue) {
                // do something here (e.g. calculate width/height, use element and its children)
                subscription.dispose(); // dispose it if you want this to happen exactly once and never again.
            });
        }
}


回答2:

After much 'googling' I have found the solution. The way to achieve this with a foreach is too register a custom event handler:

ko.bindingHandlers.runafter = {
    update: function (element, valueAccessor, allBindingsAccessor) {
        var value = valueAccessor();
        setTimeout(function (element, value) {
            if (typeof value != 'function' || ko.isObservable(value))
                throw 'run must be used with a function';

            value(element);
        }, 0, element, value);
    }
};

This handler will fire whenever an update occurs (or just once if value is not an observable).

More importantly it will fire once the foreach loop has completed!

See here.



回答3:

You could do something like this :

var viewModel = {
    items: ko.observableArray([
        { name: "one" },
        { name: "two" },
        { name: "three" }
        ]),
    myAfterAdd: function(event) {
        if (event.originalEvent.srcElement.nodeType === 1) {
           alert("myAfterAdd");   
        }
    },
    addItem: function() {
        this.items.push({ name: 'new' });
    }
};


$('#bucket').bind('DOMNodeInserted DOMNodeRemoved', viewModel.myAfterAdd);

ko.applyBindings(viewModel);

Which bucket is the ul element

See fiddle

I hope it helps