Skip to content

Update Knockout `attr` binding from DOM instead of from ViewModel

I am using Knockout.js to populate a set of HTML5 <details> elements. Here is the structure:

<div class="items" data-bind="foreach: Playlists">
    <details class="playlist-details" data-bind="attr: {id: 'playlist-details-' + $index(), open: isOpen}">
        <summary>
            <span data-bind="text: name"></span> - <span data-bind="text: count"></span> item(s)
            <div class="pull-right">
                <button data-bind="click: $parent.play, css: {disabled: count() == 0}, attr: {title: playbtn_title}" class="btn"><i class="icon-play"></i> Play</button>
                <button data-bind="click: $parent.deleteList" class="btn btn-danger"><i class="icon-trash"></i> Delete</button>
            </div>
        </summary>
        <div class="list" data-bind="with: items" style="padding-top: 2px;">
            ...
        </div>
    </details>
</div>

The data in the ViewModel looks something like this:

var VM = {
    Playlists: [
        {
            name: "My Playlist1",
            count: 3,
            items: [<LIST OF SONG ID'S>],
            playbtn_title: "Play this playlist",
            isOpen: true
        },
        {
            name: "My Playlist2",
            count: 5,
            items: [<LIST OF SONG ID'S>],
            playbtn_title: "Play this playlist",
            isOpen: null
        },
        {
            name: "My Playlist3",
            count: 0,
            items: [],
            playbtn_title: "You need to add items to this list before you can play it!",
            isOpen: null
        }
    ]
};

I have added the ability to remember the open or closed state of the details view using the isOpen property of the ViewModel and an attr binding (As originally described here).

However, when I click the <summary> to expand the details, the ViewModel does not get updated – unlike value bindings, attr bindings aren’t two-way.

How can I get this binding to update when the attribute value changes?

I know that the browser triggers a DOMSubtreeModified event when the element is opened or closed, but I;m not sure what I would put there – several things I have tried (including .notifySubscribers(), if (list.open()) ..., etc.) cause looping, where the property being changed makes the event trigger again, which changes the property again, which triggers the event again, etc.

Answer

The way that I found in the end that works is simply to have the DOMSubtreeModified “manually” update the value:

$(document).on('DOMSubtreeModified', 'details.playlist-details', function(e) {
    var list = ko.dataFor(this);
    list.open(this.getAttribute('open'));
});

(Somehow, this does not cause the looping that more-complex constructs I tried had caused.)