knockout.js and jQueryUI to create an accordion me

2020-02-04 21:06发布

问题:

Got a slight problem trying to have jquery UI and knockout js to cohoperate. Basically I want to create an accordion with items being added from knockout through a foreach (or template).

The basic code is as follows:

<div id="accordion">
    <div data-bind="foreach: items">
        <h3><a href="#" data-bind="text: text"></a></h3>
        <div><a class="linkField" href="#" data-bind="text: link"></a></div>
    </div>
</div>

Nothing impressive here... The problem is that if I do something like:

$('#accordion').accordion();

The accordion will be created but the inner div will be the header selector (first child, as default) so the effect is not the wanted one.

Fixing stuff with this:

$('#accordion').accordion({ header: 'h3' });

Seems to work better but actually creates 2 accordions and not one with 2 sections... weird.

I have tried to explore knockout templates and using "afterRender" to re-accordionise the div but to no avail... it seems to re-render only the first link as an accordion and not the second. Probably this is due to my beginner knowldge of jquery UI anyway.

Do you have any idea how to make everything work together?

回答1:

I would go with custom bindings for such functionality.

Just like RP Niemeyer with an example of jQuery Accordion binding to knockoutjs http://jsfiddle.net/rniemeyer/MfegM/



回答2:

I had tried to integrate knockout and the JQuery UI accordion and later the Bootstrap collapsible accordion. In both cases it worked, but I found that I had to implement a few workarounds to get everything to display correctly, especially when dynamically adding elements via knockout. The widgets mentioned aren't always aware of what is happening with regards to knockout and things can get messed up (div heights wrongly calculated etc...). Especially with the JQuery accordion it tends to rewrite the html as it sees fit, which can be a real pain.

So, I decided to make my own accordion widget using core JQuery and Knockout. Take a look at this working example: http://jsfiddle.net/matt_friedman/KXgPN/

Of course, using different markup and css this could be customized to whatever you need.

The nice thing is that it is entirely data driven and doesn't make any assumptions about layout beyond whatever css you decide to use. You'll notice that the markup is dead simple. This is just an example. It's meant to be customized.

Markup:

<div data-bind="foreach:groups" id="menu">
    <div class="header" data-bind="text:name, accordion: openState, click: toggle">&nbsp;</div>
    <div class="items" data-bind="foreach:items">
        <div data-bind="text:name">&nbsp;</div>
    </div>
</div>

Javascript:

ko.bindingHandlers.accordion = {

    init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        $(element).next().hide();
    },
    update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {

        var slideUpTime = 300;
        var slideDownTime = 400;

        var openState = ko.utils.unwrapObservable(valueAccessor());
        var focussed = openState.focussed;
        var shouldOpen = openState.shouldOpen;

        /*
         * This following says that if this group is the one that has 
         * been clicked upon (gains focus) find the other groups and 
         * set them to unfocussed and close them.
         */
        if (focussed) {

            var clickedGroup = viewModel;

            $.each(bindingContext.$root.groups(), function (idx, group) {
                if (clickedGroup != group) {
                    group.openState({focussed: false, shouldOpen: false});
                }
            });
        }

        var dropDown = $(element).next();

        if (focussed && shouldOpen) {
            dropDown.slideDown(slideDownTime);
        } else if (focussed && !shouldOpen) {
            dropDown.slideUp(slideUpTime);
        } else if (!focussed && !shouldOpen) {
            dropDown.slideUp(slideUpTime);
        }
    }
};

function ViewModel() {

    var self = this;
    self.groups = ko.observableArray([]);

    function Group(id, name) {

        var self = this;
        self.id = id;
        self.name = name;

        self.openState = ko.observable({focussed: false, shouldOpen: false});

        self.items = ko.observableArray([]);

        self.toggle = function (group, event) {
            var shouldOpen = group.openState().shouldOpen;
            self.openState({focussed: true, shouldOpen: !shouldOpen});
        }
    }

    function Item(id, name) {
        var self = this;
        self.id = id;
        self.name = name;
    }

    var g1 = new Group(1, "Group 1");
    var g2 = new Group(2, "Group 2");
    var g3 = new Group(3, "Group 3");

    g1.items.push(new Item(1, "Item 1"));
    g1.items.push(new Item(2, "Item 2"));

    g2.items.push(new Item(3, "Item 3"));
    g2.items.push(new Item(4, "Item 4"));
    g2.items.push(new Item(5, "Item 5"));

    g3.items.push(new Item(6, "Item 6"));

    self.groups.push(g1);
    self.groups.push(g2);
    self.groups.push(g3);
}

ko.applyBindings(new ViewModel());


回答3:

Is there any reason why you can't apply the accordion widget to the inner div here? For example:

<div id="accordion" data-bind="foreach: items">
    <h3><a href="#" data-bind="text: text"></a></h3>
    <div><a class="linkField" href="#" data-bind="text: link"></a></div>
</div>


回答4:

I attempted the accepted solution and it worked. Just had to make a little change since i was getting following error

Uncaught Error: cannot call methods on accordion prior to initialization; attempted to call method 'destroy'

just had to add following and it worked

if(typeof $(element).data("ui-accordion") != "undefined"){
$(element).accordion("destroy").accordion(options);
}

for details please see Knockout accordion bindings break



回答5:

You could try this to template it, similar to this:

<div id="accordion" data-bind="myAccordion: { },template: { name: 'task-template', foreach: ¨Tasks, afterAdd: function(elem){$(elem).trigger('valueChanged');} }"></div>   

<script type="text/html" id="task-template">
    <div data-bind="attr: {'id': 'Task' + TaskId}, click: $root.SelectedTask" class="group">
        <h3><b><span data-bind="text: TaskId"></span>: <input name="TaskName" data-bind="value: TaskName"/></b></h3>
         <p>
             <label for="Description" >Description:</label><textarea name="Description" data-bind="value: Description"></textarea>
          </p> 
     </div>
 </script>

"Tasks()" is a ko.observableArray with populated with task-s, with attributes "TaskId", "TaskName","Description", "SelectedTask" declared as ko.observable();

"myAccordion" is a

ko.bindingHandlers.myAccordion = {
    init: function (element, valueAccessor) {
        var options = valueAccessor();
        $(element).accordion(options);
        $(element).bind("valueChanged", function () {
           ko.bindingHandlers.myAccordion.update(element, valueAccessor);
       });
      ...
}


回答6:

What I did was, since my data was being loaded from AJAX and I was showing a "Loading" spinner, I attached the accordion to ajaxStop like so:

$(document).ajaxStart(function(){$("#cargando").dialog("open");}).ajaxStop(function(){$("#cargando").dialog("close");$("#acordion").accordion({heightStyle: "content"});});

Worked perfectly.