knockout.js and jQueryUI to create an accordion menu
Asked Answered
S

6

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?

Sylphid answered 27/1, 2012 at 16:6 Comment(0)
L
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/

Lalittah answered 27/1, 2012 at 20:16 Comment(6)
Yup - that's some serious accordioning.Heteromerous
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!Sylphid
In my opinion custom bindings are essential for knockout understanding and usingLalittah
updated AlfeG jsfiddle using knockout template , and knockout 2.1 jsfiddle.net/coffeedannylai/neYHw/9Goingover
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.Smolensk
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.Smolensk
O
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());
Observable answered 30/1, 2013 at 18:13 Comment(2)
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!)Can
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.Expedient
V
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>
Vary answered 12/10, 2012 at 16:53 Comment(0)
X
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

Xenophobe answered 23/7, 2014 at 19:2 Comment(0)
E
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);
       });
      ...
}
Eclecticism answered 25/9, 2013 at 14:40 Comment(0)
H
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.

Hygrometric answered 18/7, 2014 at 19:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.