I'm binding a nested listview with knockoutjs and styling it with JQuery mobile like this
<ul id="machine-list" data-bind="foreach: machineList" data-role="listview">
<li><span data-bind="text:location"></span>
<ul data-bind="foreach: machines" data-role="listview">
<li><span data-bind="text:machine"></span></li>
</ul>
</li>
</ul>
When it goes to the nested list there's nothing there. I know the binding is working because if I remove the data-role I get the nested list. Can anyone help? Cheers.
If you're using the latest version of jQuery Mobile, you must know that nested lists have only partial support. Here's the source link.. As mentioned we'll have to use the page -> page navigation system by ourselves, so here goes.
Setup
I built up an example for this sometime back. You could adapt your code to that.
I have a json object array like this :
[
{
"name": "DC Comics",
"characters": [
"All-Star Squadron",
"Birds of Prey",
...
]
},
{
"name": "Marvel Studios",
"characters": [
"Alpha Flight",
"Avengers",
"Champions",
"Defenders",
...
]
}
....
]
So, it might be better if you have two pages, one for showing the main list aka the groups and when clicked, another page to show the sub items ie., the characters.
On page load, I'd like to show something like this :
And when one of those links are clicked, I'd like to show the stuff which is present inside characters array. Like for example, if I choose "DC Comics", the current context would be :
{
"name": "DC Comics",
"characters": [
"All-Star Squadron",
"Birds of Prey",
...
]
}
From here, I'd take out the characters property and show it out in a list in a new page. (This is what nested lists used to do, anyway). So, something like this is what is needed:
Two pages, Two ViewModels
Now to achieve this, there might be ways. But the best method would be to use multiple ViewModels, one for each page, so making it 2 and bind it when that particular page is initialised. So for a particular page,
Markup :
<div data-role="page" id="groups-page"></div>
View Model :
We'd have a seperate ViewModel:
//view model for the parent page.
var groupModel = {
//data array
groupInfo: [
{
"name": "DC Comics",
"characters": [
"All-Star Squadron",
"Birds of Prey",
...
]
},
{
"name": "Marvel Studios",
"characters": [
"Alpha Flight",
"Avengers",
"Champions",
"Defenders",
...
]
}
....
]
}
pageinit event :
And bind it in pageinit event of #group-page:
//events pertaining to Page 1 - the groups
$(document).on({
"pageinit": function () {
//apply partial binding to groups-page alone. Reason we're doing this is because only this page will be available onload
ko.applyBindings(groupModel, this);
}
}, "#groups-page");
Note that the second object in applyBindings (this) indicates the DOM element with id set to groups-page.
Similarly, you'd do the same thing for the second page, #character-page :
Markup :
<div data-role="page" id="character-page"></div>
ViewModel :
//view model for the character page - basically an empty view model to be filled with data when a link is clicked.
var characterModel = {
name: ko.observable(),
characterInfo: ko.observableArray()
}
pageinit event :
//events pertaining to Page 1 - the groups
$(document).on({
"pageinit": function () {
//apply partial binding to character-page alone.
ko.applyBindings(groupModel, this);
}
}, "#character-page");
The reason we're using observables here is because everytime something is clicked, we'd be updating the observables which will automatically change the content of the second page listview.
Populating the first page
This is how my HTML structure looks like :
<ul data-bind="foreach: groupInfo">
<li>
<a href="#">
<span data-bind="text: name"></span>
<span data-bind="text: characters.length" class="ui-li-count"></span>
</a>
</li>
</ul>
And after applyBindings() is bound only to the first page, I'd call a refresh on those listviews in the page :
//apply partial binding to groups-page alone. Reason we're doing this is because only this page will be available onload
ko.applyBindings(groupModel, this);
//refresh the listview
$("ul", this).attr("data-role", "listview").listview().listview("refresh");
This would lie in the pageinit event of the #group-page.
Click event of the groups, changePage to characters
For this, I'd use the click binding of KO on the a tag of the listview in first page:
<a href="#" data-bind="click: $root.getInfo">
<span data-bind="text: name"></span>
<span data-bind="text: characters.length" class="ui-li-count"></span>
</a>
where $root is the context in ViewModel level (see docs).
Oh wait! getInfo must be added to groupModel, the model for the first page. So the viewmodel is changed to this :
//view model for the parent page.
var groupModel = {
//data array
groupInfo: [
//groups
],
//click event to be fired when an anchor tag is clicked
getInfo: function (data, e) {
//here "data" variable gives the clicked elements' corresponding array and "e" gives the event object
//prevents defaultbehaviour of anchor tag
e.preventDefault();
//setting up the character array in View model of page 2
characterModel.characterInfo(data.characters);
//setting up name variable of ViewModel in page 2
characterModel.name(data.name);
//change the page
$.mobile.changePage("#character-page", {
transition: "slide"
});
}
}
Note that I'm populating the characterModel only now. Since the properties inside this model are observables, every time these variables are updated, DOM is also updated. The HTML of the second page is :
<ul data-bind="foreach: characterInfo">
<li data-bind="text: $data"></li>
</ul>
$data is used to get the stuff inside characterInfo, if you didn't know. When this is done, you'd have something like this :
The next step would be to refresh the listview, but not in pageinit. Reason being, pageinit is fired only once and since character-page gets updated every single time a click happens, you'd have use a pageshow or pagebeforshow method. More info here. Now the events bound to the character-page are :
//events pertaining to Page 2 - the characters
$(document).on({
"pageinit": function () {
ko.applyBindings(characterModel, this);
},
"pagebeforeshow": function () {
//refresh listview before page is shown
$("ul", this).attr("data-role", "listview").listview().listview("refresh");
}
}, "#character-page");
The above snip would refresh your styles everytime the pagebeforeshow event of "#character-page" is triggered (which is what we want) and finally give you this :
So that's it! Hope this helped :)
And, before we forget, here's a demo.
Related
I would like to show a tooltip on a text input that has a ui-state-disabled class.
I took a peek to the tooltip source code and I couldn't find something that checks against that particular class. So I don't know why it won't show.
As far as I can tell, the elements aren't disabled per se, they just have a class applied to them.
So, how can I show a tooltip on elements that have that class? I don't want to use a wrapper or anything like that. Maybe extending through widget factory...
Here's a sample code
HTML
<input name="#1" class="text" data-tooltip="message A">
<input name="#2" class="text" data-tooltip="message B">
<br>
<button id="disable">disable input #2</button>
<button id="enable">enable input #2</button>
JS
$(".text").each(function()
{
$(this).tooltip({
content: $(this).data("tooltip"),
items: ".text"
});
});
$("#disable").click(function()
{
$("input[name='#2']").addClass("ui-state-disabled");
});
$("#enable").click(function()
{
$("input[name='#2']").removeClass("ui-state-disabled");
});
FIDDLE: https://jsfiddle.net/hn1o4qs2/
See the doc (http://api.jqueryui.com/tooltip/):
In general, disabled elements do not trigger any DOM events.
Therefore, it is not possible to properly control tooltips for
disabled elements, since we need to listen to events to determine when
to show and hide the tooltip. As a result, jQuery UI does not
guarantee any level of support for tooltips attached to disabled
elements. Unfortunately, this means that if you require tooltips on
disabled elements, you may end up with a mixture of native tooltips
and jQuery UI tooltips.
Solution with wrapper
Your HTML:
<span class="input-container" data-tooltip="message A">
<input name="#1" class="text">
</span>
<span class="input-container" data-tooltip="message B">
<input name="#2" class="text">
</span>
<br>
<button id="disable">
disable input #2
</button>
<button id="enable">
enable input #2
</button>
Your Javascript
$(".input-container").each(function()
{
$(this).tooltip({
content: $(this).data("tooltip"),
items: ".input-container"
});
});
// ... The rest is the same
Solution with fake disabled-property
Here you can use a readonly attribute and a custom class for disabled input.
Playground: https://jsfiddle.net/5gkx8qec/
As I've stated in my question, I needed to get this working without adding a container or anything like that. And I was willing to extend the widget somehow...
So I read the source code more carefully and searched throught the whole repository for ui-state-disabled, and found that in widget.js there is an _on() method that at some point performs a check against that class and a flag called suppressDisabledCheck
A comment in code says
// Allow widgets to customize the disabled handling
// - disabled as an array instead of boolean
// - disabled class as method for disabling individual parts
This was very important, it gave me the clue that this check could be overriden. So a quick search in google and the widget factory had the answer:
Automatically handles disabled widgets: If the widget is disabled or
the event occurs on an element with the ui-state-disabled class, the
event handler is not invoked. Can be overridden with the
suppressDisabledCheck parameter.
So basically I did this:
$.widget("ui.tooltip", $.ui.tooltip,
{
options: {
allowOnDisabled: false
},
_on: function()
{
var instance = this;
this._super(instance.options.allowOnDisabled, {
mouseover: "open",
focusin: "open",
mouseleave: "close",
focusout: "close"
});
}
});
And then used it like this:
$(".text").each(function()
{
$(this).tooltip({
allowOnDisabled: true,
content: $(this).data("tooltip"),
items: ".text"
});
});
EDIT 2022-09-15
I was having some trouble with this implementation, so I've changed it a little bit
$.widget("ui.tooltip", $.ui.tooltip,
{
options: {
allowOnDisabled: false
},
_create: function()
{
this._super();
var instance = this;
this._on(instance.options.allowOnDisabled, {
mouseover: "open",
focusin: "open",
mouseleave: "close",
focusout: "close"
});
}
});
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]);
});
});
});
I'm trying to populate the content of JQM accordions/collapsible sets dynamically. I've had a look at http://the-jquerymobile-tutorial.org/jquery-mobile-tutorial-CH20.php, but the examples seem to be outdated. So I've tried to come up with a working example, but I'm currently stuck and can't see, why this shouldn't work:
<script type="text/javascript">
$(document).ready(function() {
$(".mySet").bind('expand', function (event, ui) {
/* This prints e.g. "This is the first entry\n
I'm the collapsible set content for section 1." */
console.log($(this).text());
/* This should print "This is the first entry" (=the innerText of <h3>)
but it doesn't, it prints "This is the first entry\n
I'm the collapsible set content for section 1." as well */
console.log($(this).next().text());
/* This should just print "I'm the collapsible set content for section 1"
(=the innerText of <p>) but it doesn't, it prints e.g. "This is the
first entry\n I'm the collapsible set content for section 1." as well */
console.log($(this).nextAll("p").text());
});
});
</script>
<div data-role="collapsible-set">
<div data-role="collapsible" class="mySet">
<h3>This is the first entry</h3>
<p>I'm the collapsible set content for section 1.</p>
</div>
<div data-role="collapsible" class="mySet">
<h3>This is the second entry</h3>
<p>I'm the collapsible set content for section 2.</p>
</div>
</div>
Does anyone see, why jQuery won't descend on $(this) (=which points to the currently expanded <div ...class="mySet">? If I debug this code in Opera's DragonFly I can see, that $(this) seems to be the same as $(this).next() (at least they have the same values for innerHTML etc.)
Thanks for your help!
So, after fiddling around a little bit I came up with the following hack/workaround:
$(document).ready(function() {
$(".mySet").bind('expand', function (event, ui) {
/* I'm just creating a new var "myDiv" which points to my current div (so in a
way "myDiv" is just a copy of "this" */
var myDiv = $("#"+$(this).attr('id'));
var myP = myDiv.find('p:first');
// Works as expected, prints "I'm the collapsible set content for section 1."
console.log(myP.text());
});
});
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.
I have a list like so:
<ol id="page_items">
<li>
<label for="page_body_1">Content Area 1</label>
<textarea name="page_body_1" class="page_content_area" rows="10"></textarea>
</li>
<li>
<label for="page_body_2">Content Area 2</label>
<textarea name="page_body_2" class="page_content_area" rows="10"></textarea>
</li>
</ol>
When the page loads, #page_items turns into a tinyMCE editor. What I want is for the element that defines whether or not the li elements are being sorted to be the <label> but no other child elements of li. So the only element that starts the sort is the label.
Here's my jQuery:
$(document).ready(function(){
$("#page_items").sortable({
activate: function(event, ui) {
var EditorID = ui.item.find('textarea').attr('id');
if ( EditorID ){
tinyMCE.execCommand("mceRemoveControl", false, EditorID);
$('#'+EditorID).hide();
}
},
stop: function(event, ui) {
var EditorID = ui.item.find('textarea').attr('id');
if ( EditorID ){
$('#'+EditorID).show();
tinyMCE.execCommand("mceAddControl", false, EditorID);
delete EditorID;
}
}
});
});
In case anyone is wondering, I'm disabling the tinyMCE because in FireFox, moving an iFrame around the DOM clears it's contents and doesn't allow focus back on it.
Is there a way to cancel the sortable if the element clicked isn't the label?
If anyone has any code clean-up suggestions they are also welcome!
Thanks.
This turned out to be a sortable option that I didn't see before (I looked... oh I looked). The handle option is what I need. This initializes a sortable with the handle option specified.
Simply...
$(document).ready(function(){
$("#page_items").sortable({
handle: 'label'
});
});