Having trouble with jQueryUI accordion and Knockoutjs - jquery-ui

I've been able to replicate the problem here: http://jsfiddle.net/NE6dm/
I have the following HTML which I'm using in an app:
<div data-bind="foreach: items, jqAccordion: { active: false, collapsible: true }">
<h3>
</h3>
<div>
hello
</div>
</div>
<button title="Click to return to the complaints list." data-bind="click: addItem">Add Item</button>
The idea is to display an accordion for a bunch of items that will dynamically be added/removed via a Knockout observable array.
Here's some JavaScript code which I use:
// Tab.
var tab = function (questionSet) {
this.id = questionSet.code;
this.title = questionSet.description;
this.questionSet = questionSet;
};
Custom Knockout binding handler:
ko.bindingHandlers.jqAccordion = {
init: function (element, valueAccessor) {
var options = valueAccessor();
$(element).accordion(options);
$(element).bind("valueChanged", function () {
ko.bindingHandlers.jqAccordion.update(element, valueAccessor);
});
},
update: function (element, valueAccessor) {
var options = valueAccessor();
$(element).accordion('destroy').accordion(options);
}
};
var NonSequentialViewModel = function () {
var items = ko.observableArray();
items.push(new tab({ id: 23, description : 'Added Inline' }));
var addItem = function() {
items.push(new tab({ id: 5, description: 'Added by a click' }));
};
return {
addItem: addItem,
items: items
}
}
var nonsequentialViewModel = new NonSequentialViewModel();
ko.applyBindings(nonsequentialViewModel);
Now the problem is this - when I view the HTML page, the item 'Added Inline' appears fine, in that I can collapse and expand it. However, when I click the button 'Add Item', a new item is added to he accordion, but it has not styling at all. For example:
In the above image, the first item is styled correctly, however the remaining items have none of the jQuery UI styling applied. Basically, any item which is added dynamically does not have any accordion styling applied.
I have seen this question
knockout.js and jQueryUI to create an accordion menu
and I've tried using the jsFiddle included in the question, but I cannot see why my code doesn't have the same result.
I'm hoping someone else has experienced this before and can help.
EDIT:
I've looked into this further and see that the problem is this - when I add a new item to the oservable array, the custom handler's update method is not executed. Thus the redrawing of the accordion never happens.
I can't see why the update should not be called. This is realyl doing my head in! :)
EDIT:
I've been able to replicate the problem here: http://jsfiddle.net/NE6dm/

Your NonSequentialViewModel constructor doesn't return items array. Update return statement to this:
return {
items: items,
addItem: addItem
}
Here is working fiddle: http://jsfiddle.net/vyshniakov/MfegM/323/

Old question, but I believe I was having the same problem.
I may need to submit a bug to knockout.js. I just spend several hours trying to figure out similar problems.
In short... if I load your jsfiddle and change the version of knockout to 2.1.0, it appears to work fine.
this:
<script type="text/javascript" src="http://cloud.github.com/downloads/SteveSanderson/knockout/knockout-2.2.0.debug.js"></script>
to this:
<script type="text/javascript" src="http://cloud.github.com/downloads/SteveSanderson/knockout/knockout-2.1.0.debug.js"></script>
(The only difference being the version 2.2.0 --> 2.1.0)
Further... I ended up settling on several versions:
jquery: 1.9.1
jquery-ui (combined): 1.9.2
knockoutjs: 2.1.0

Related

Knockout Binding Not Working with jQueryUI Dialogue

My viewModel has an array called 'Items'. I want to display the contents of 'Items' using a foreach binding. Everything works fine when I use regular HTML. But does not work with a dialogue box which I created using jQueryUI.
HTML:
<div id="skus0">
<div id="skus1">
<ul data-bind="foreach: Items">
<li data-bind="text:Name"></li>
</ul>
</div>
<input type="button" id="openQryItems" class="btn btn-info" value="Open" data-bind="click:openQueryItems" />
</div>
JavaScript:
// my view model
var viewModel = {
Items: [{Name:'Soap'},{Name:'Toothpaste'}]
};
// JS to configure dialogue
$("#skus1").dialog({
autoOpen: false,
width: 500,
modal: true,
buttons: {
"OK": function () {
$(this).dialog("close");
},
"Cancel": function () {
$(this).dialog("close");
}
}
});
// for mapping my model using ko.mapping plugin
var zub = zub || {};
zub.initModel = function (model) {
zub.cycleCountModel = ko.mapping.fromJS(model);
zub.cycleCountModel.openQueryItems = function () {
$("#skus1").dialog("open");
}
ko.applyBindings(zub.cycleCountModel, $("#skus0")[0]);
}
zub.initModel(viewModel);
I have created a fiddle here my fiddle
$.fn.dialog removes the element from its place in the DOM and places it in a new container; this is how it can create a floating window. The problem with this happening is that it breaks data binding, since the dialog DOM is no-longer nested within the top-level data-bound DOM.
Moving the dialog initialization to after ko.applyBindings will enable dialog to yank stuff out of the DOM after the list is populated. Of course, this means that after that point, future changes will still not be reflected, which may be important if you're wanting the opened dialog to change automatically.
If you are wanting the dialog contents to be fully dynamic, you could create a binding handler; we did this in our project. Here's a rough outline of how we did this:
ko.bindingHandlers.dialog = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingCtx) {
var bindingValues = valueAccessor();
var hasAppliedBindings = false;
var elem = $(element);
var options = {
id: ko.utils.unwrapObservable(bindingValues.id),
title: ko.utils.unwrapObservable(bindingValues.title),
// etc...
onOpen: function () {
if (!hasAppliedBindings) {
hasAppliedBindings = true;
var childCtx = bindingCtx.createChildContext(viewModel);
ko.applyBindingsToDescendants(childCtx, element);
}
}
};
elem.dialog(options);
}
return { controlsDescendantBindings: true };
}
...which we used like this:
<div data-bind="dialog: { title: 'some title', id: 'foo', ... }">
<!-- dialog contents -->
</div>
What return { controlsDescendantBindings: true } does is makes sure that outer bindings do not affect anything using the dialog binding handler. Then we create our own Knockout binding "island" after it is pulled out of the DOM, based on the original view model.
Although in our project we also used hybrid jQuery+Knockout, I would highly recommend you avoid this whenever possible. There were so many hacks we had to employ to sustain this type of application. The very best thing you should do is prefer Knockout binding handlers (and I think it has a "component" concept now which I haven't played with) over DOM manipulations to avoid buggy UI management.

Initializing knockoutjs content in child jquery mobile page

I know there are a lot of questions that cover jquery mobile / knockoutjs integration, however I couldn't find a thread that solved my issue. I have a master view model which contains child view models, and so I initialize this on page load, as that event is only fired on application load:
var viewModel = null;
$(function () {
console.debug("running init");
viewModel = new ViewModel();
ko.applyBindings(viewModel);
});
This works great on the first page of my app, however when I go to a child page, the knockoutjs content doesn't show up because jquery mobile has loaded the html dynamically and knockout doesn't know to update the binded content. I'm trying to tell it to update dynamically by using the $(document).delegate function, however I'm struggling with how it's supposed to be implemented.
<ul id="speeding" data-role="listview" data-bind="foreach: speeding.items">
<li>
<h3 class="ui-li-heading" data-bind="text: Address"></h3>
<p class="ui-li-desc" data-bind="text: Address2"></p>
<p class="ui-li-desc" data-bind="text: PrettyDate"></p>
<p class="ui-li-aside" data-bind="text: SpeedMph"></p>
</li>
<script type="text/javascript">
var loaded = false;
$(document).delegate("#page-speeding", "pagebeforecreate", function () {
if (!loaded) {
loaded = true;
ko.applyBindings(viewModel);
}
else {
$("#speeding").trigger("refresh");
}
});
</script>
</ul>
I'm putting the delegate function within the page it's being called on, as apparently that's a requirement of using delegate. Then on first load of this child page I call ko.applyBindings (I only wanted to call this on application load but I couldn't get trigger("create") to work. On subsequent calls it would call trigger("refresh") (which doesn't work for me.) The issue though is that the delegate function gets added each time I go to the child page. So on first load of the child page, it will call the delegate callback function once. If I go back to the main page, then back to the child page, it will call the delegate callback twice, and so on.
Can someone please provide guidance of the recommended approach to refreshing the knockoutjs bindings on child pages?
This is what ended up working for me. I have no idea if there's a better way or not...
var viewModel = null;
$(function () {
console.debug("running init");
viewModel = new ViewModel();
ko.applyBindings(viewModel);
var pages = [
"scorecard", "speeding", "leaderboard"
];
_.each(pages, function (page) {
$(document).on("pagebeforecreate", "#page-" + page, function () {
console.debug("applying " + page + " bindings");
ko.applyBindings(viewModel, $("#page-" + page)[0]);
});
});
});

Optimal way to save order of jQueryUI sortable list with Meteor

I need some guidance/suggestions for an optimal way to save the order of a sortable list that takes advantage of Meteor.
The following is a scaled down version of what I'm trying to do. The application is a simple todo list. The end goal for the user is to sort their list where the data is picked up from the database. As the user sorts tasks, I would like to save the order of the tasks.
I've implemented this application without Meteor using php/ajax calls using sortable's update event that would delete the entry in the database and replace it with what was currently in the DOM. I'm curious to know if there are a better ways to do this taking advantage of Meteor's capabilities.
The following sample code is straight off of a live demo.
HTML:
<template name="todo_list">
<div class="todo_list sortable">
{{#each task}}
<div class="task">
<h1>{{title}}</h1>
{{description}}
</div>
{{/each}}
</div>
</template>
JS(Without the Meteor.isServer that simply populates the database.):
if (Meteor.isClient) {
//Populate the template
Template.todo_list.task = function () {
return Tasks.find({});
};
//Add sortable functionality
Template.todo_list.rendered = function () {
$( ".sortable" ).sortable();
$( ".sortable" ).disableSelection();
};
}
Sample data (Output of Tasks.find({})):
[{
title:"CSC209",
description:"Assignment 3"
},
{
title:"Laundry",
description:"Whites"
},
{
title:"Clean",
description:"Bathroom"
}]
You'd probably want to first sort your items by a new field on you collection then, you'll want to hook into the jQuery sortable update event:
if (Meteor.isClient) {
// Populate the template
Template.todo_list.task = function () {
return Tasks.find({}, { sort: ['order'] });
};
// Add sortable functionality
Template.todo_list.rendered = function () {
$('.sortable').sortable({
update: function (event, ui) {
// save your new list order based on the `data-id`.
// when you save the items, make sure it updates their
// order based on their index in the list.
some_magic_ordering_function()
}
});
$( ".sortable" ).disableSelection();
};
}
You template would look a bit like this:
<template name="todo_list">
<div class="todo_list sortable">
{{#each task}}
<div class="task" data-id="{{_id}}">
<h1>{{title}}</h1>
{{description}}
</div>
{{/each}}
</div>
</template>
And when that event is triggered, it would determine the order of the list and save the new order in the documents for the collection.
This isn't really a complete answer, but hopefully it helps a bit.

knockout.js and jQueryUI to create an accordion menu

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></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?
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/
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"> </div>
<div class="items" data-bind="foreach:items">
<div data-bind="text:name"> </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());
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></h3>
<div><a class="linkField" href="#" data-bind="text: link"></a></div>
</div>
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
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);
});
...
}
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.

jQuery UI Tabs: URL instead of AJAX does not work properly

I wrote like there: http://jqueryui.com/demos/tabs/#...follow_a_tab.27s_URL_instead_of_loading_its_content_via_ajax
<script type="text/javascript">
$(function(){
$("#tabs").tabs({
select: function(event, ui) {
var url = $.data(ui.tab, 'load.tabs');
if( url ) {
location.href = url;
return false;
}
return true;
}
});
});
</script>
<div id="tabs">
<ul>
<li>
Home
</li>
<li>
About
</li>
</ul>
</div>
Tabs are created, but initial list (div, ul, li) is visible as well. Another problem: when I hover over tab, I see URL kind of /default.htm#ui-tabs-1, /default.htm#ui-tabs-2 etc. But I want to see URL "/default.htm" over the 1st tab and URL "/about.htm" over the 2nd tab.
What could I do to solve my problem?
UPDATE
In version 1.9 there is powerful widget "menu".
You are miss interpreting the jQuery UI Tabs.
This Tabs are for having content hide/show and if using ajax pull the page info and show it on demand.
if you want those tabs to act as a menu ... then you need a menu, not the jQuery UI Tabs.
If your idea if to use this tabs but to fetch the /about.htm as a new content, then you can use the ajax example
http://jqueryui.com/demos/tabs/#ajax
keep in mind that it will fetch the entire content, so the /about.htm page should not have <html> neither <body> tags
I don't want to encourage you to do this, but the solution is currently available on jQuery UI's website: http://jqueryui.com/demos/tabs/#...follow_a_tab.27s_URL_instead_of_loading_its_content_via_ajax
You may extend it a bit to follow only certain URLs:
select: function(e, ui)
{
var tab = $(ui.tab);
var url = $.data(ui.tab, 'load.tabs');
if(url && tab.attr('data-ajax') == 'false')
{
location.href = url;
return false;
}
return true;
}
Then, when defining tabs:
<li>...</li>

Resources