11

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?

Tallmaris
  • 7,485
  • 3
  • 25
  • 57

6 Answers6

13

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/

Community
  • 1
  • 1
AlfeG
  • 1,461
  • 3
  • 18
  • 33
  • 3
    Yup - that's some serious accordioning. – PhillipKregg Jan 27 '12 at 20:19
  • This is interesting and it answers what I was asking even though it seems a little hacky :/ I probably need to look into custom bindings a bit better to fully understand. Thanks for the link anyway! – Tallmaris Jan 31 '12 at 13:36
  • In my opinion custom bindings are essential for knockout understanding and using – AlfeG Jan 31 '12 at 13:53
  • 3
    updated AlfeG jsfiddle using knockout template , and knockout 2.1 http://jsfiddle.net/coffeedannylai/neYHw/9/ – dan_l Jul 30 '12 at 01:29
  • I get an error from knockout.js when I put both a foreach binding and an accordion binding on the same element saying `Multiple bindings (foreach and accordion) are trying to control descendant bindings of the same element. You cannot use these bindings together on the same element.` – Trevor Elliott Aug 27 '13 at 20:24
  • I found a solution to my problem. I nested the foreach binding inside the accordion div using the containerless foreach syntax (HTML comments) and then set the accordion binding's `refreshOn` option to the observable array. This way it refreshes the accordion widget when the array changes. – Trevor Elliott Aug 28 '13 at 13:39
6

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());
Matt Friedman
  • 1,465
  • 1
  • 17
  • 23
  • Really nice. Thanks for this. I modified it slightly to provide useful keyboard navigation for my users. (At least more useful keyboard navigation than most jQuery-based menu implementations provide!) – David Montgomery May 03 '13 at 00:15
  • Thank you for this. I ended up using it as well. It'd be awesome if there was a way to make it a little more generic. Maybe I'll play around with it later. – Quickhorn Oct 11 '13 at 22:17
1

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>
Joe Lee-Moyet
  • 1,516
  • 1
  • 18
  • 24
1

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

Community
  • 1
  • 1
ATHER
  • 2,725
  • 4
  • 31
  • 50
0

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);
       });
      ...
}
Asle G
  • 568
  • 7
  • 25
0

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.