How to make a collapsible split list in jQuery Mobile

Introduction:

Screen real estate is valuable, especially on a mobile device. If you have a lot of information you want to display that can be grouped under a common header / theme, a great solution is to use a collapsible listview found in the jQuery Mobile library.
 
However, what do you do when you want to combine the functionality of a collapsible list with that of something that has a common button among the list items that would allow you to edit / delete / perform some action on the list item that is common to all the items on the list (like a jQuery Mobile Split List)?
 
The following tutorial will walk you through on how to create a collapsible split list in jQuery Mobile and will utilize some custom CSS and JavaScript to get the job done.

Tutorial

Lets start off with a basic split list, taken and modified from the jQuery Mobile Listview tutorial (converted listview to split list, changes highlighted):

<ul data-role="listview" data-inset="true" data-split-icon="delete">
	<li>
		<a href="#">
			<h2>Stephen Weber</h2>
			<p><strong>You've been invited to a meeting at Filament Group in Boston, MA</strong></p>
			<p>Hey Stephen, if you're available at 10am tomorrow, we've got a meeting with the jQuery team.</p>
		</a>
		<a href="#">Delete</a>
	</li>
	<li>
		<a href="#">
			<h2>jQuery Team</h2>
			<p><strong>Boston Conference Planning</strong></p>
			<p>In preparation for the upcoming conference in Boston, we need to start gathering a list of sponsors and speakers.</p>
		</a>
		<a href="#">Delete</a>
	</li>
	<li>
		<a href="#">
			<h2>Avery Walker</h2>
			<p><strong>Re: Dinner Tonight</strong></p>
			<p>Sure, let's plan on meeting at Highland Kitchen at 8:00 tonight. Can't wait!</p>
		</a>
		<a href="#">Delete</a>
	</li>
</ul>

Using the code above, you should get something similar to the following:
(note the following is using jQuery Mobile 1.4.2 and jQuery 1.9.1, yours may look different if you’re using a different theme / version of jQuery Mobile / jQuery)
 

 
In this contrived example, that’s a decent amount of potentially useful information. However, on a mobile device, the amount that is displayed at one time is limited and if the user was looking for something at the bottom, they would have to scroll through the content of everything else. Lets add another div to enclose the content we want to hide:
(changes highlighted)

<ul data-role="listview" data-inset="true" data-split-icon="delete">
	<li>
		<a href="#">
			<h2>Stephen Weber</h2>
			<div style="display:none;">
				<p><strong>You've been invited to a meeting at Filament Group in Boston, MA</strong></p>
				<p>Hey Stephen, if you're available at 10am tomorrow, we've got a meeting with the jQuery team.</p>
			</div>
		</a>
		<a href="#">Delete</a>
	</li>
	<li>
		<a href="#">
			<h2>jQuery Team</h2>
			<div style="display:none;">
				<p><strong>Boston Conference Planning</strong></p>
				<p>In preparation for the upcoming conference in Boston, we need to start gathering a list of sponsors and speakers.</p>
			</div>
		</a>
		<a href="#">Delete</a>
	</li>
	<li>
		<a href="#">
			<h2>Avery Walker</h2>
			<div style="display:none;">
				<p><strong>Re: Dinner Tonight</strong></p>
				<p>Sure, let's plan on meeting at Highland Kitchen at 8:00 tonight. Can't wait!</p>
			</div>
		</a>
		<a href="#">Delete</a>
	</li>
</ul>

Resulting in:
 

 
The bulk of the content is gone, and the height changed from 307 px to 174 px, a difference of 133 px or ~44 px per list item. That’s a lot of recovered space…but how do we get the content back?
 
For that, we’ll need to write some custom javascript. What we need to do is target the link that was clicked, and un-hide the div that we had hidden. Luckily, this is very easy:

$(document).on("click","ul a", function(){
	$(this).children("div:last").toggle(200);
	if($(this).attr("title")) {
		console.log("clicked delete");
	} else {
		//Clicked the link to toggle the content
	}
});

Brief explanation:

$(document).on("click","ul a", function(){

This defines a function to be called whenever a link is clicked within an unordered list.

$(this).children("div:last").toggle(200);

$(this) refers to the link we’ve selected, .children(“div:last”) targets the last div element that is nested within the link (more on this in a minute), and .toggle(200) tells jQuery to hide/unhide the div, animating it over 200 ms.

if($(this).attr("title")) {

The text we put inside the link for the delete icon is used to populate the title attribute (in this case “Delete”). Since no title was supplied for the collapsible content, the title attribute will not be set, and the code within this if-statement will only be executed when the delete icon is clicked. If you provided a title attribute for the collapsible content, you could easy filter it out by doing a comparison in the if-statement.


When combined with the code above, we should get something that looks like this:
 

 
When you click on each of the list items, it’ll expand and collapse, displaying and hiding the content. Also, if you’re using Chrome, you can look in the browser console and change from “<top frame>” to “collapsible-split-list-3.html” and you’ll see “clicked delete” appear whenever you click the delete icon. Other browsers that allow the viewing of the console should also show “clicked delete”, but the method to find that is outside the scope of this tutorial.
 
Now, in the above javascript code, why did I specifically target the LAST div when there’s only 1 div to begin with? Because currently, there isn’t any indication that the list items can be clicked to display more information, and since jQuery Mobile doesn’t natively support collapsible split lists / multiple icons for lists, we need to add another div to put the icon in there:
The HTML (changes highlighted)

<ul data-role="listview" data-inset="true" data-split-icon="delete">
	<li>
		<a href="#"><div class="iconFloat ui-btn ui-corner-all ui-icon-plus ui-btn-icon-notext"></div>
			<h2>Stephen Weber</h2>
			<div style="display:none;">
				<p><strong>You've been invited to a meeting at Filament Group in Boston, MA</strong></p>
				<p>Hey Stephen, if you're available at 10am tomorrow, we've got a meeting with the jQuery team.</p>
			</div>
		</a>
		<a href="#">Delete</a>
	</li>
	<li>
		<a href="#"><div class="iconFloat ui-btn ui-corner-all ui-icon-plus ui-btn-icon-notext"></div>
			<h2>jQuery Team</h2>
			<div style="display:none;">
				<p><strong>Boston Conference Planning</strong></p>
				<p>In preparation for the upcoming conference in Boston, we need to start gathering a list of sponsors and speakers.</p>
			</div>
		</a>
		<a href="#">Delete</a>
	</li>
	<li>
		<a href="#"><div class="iconFloat ui-btn ui-corner-all ui-icon-plus ui-btn-icon-notext"></div>
			<h2>Avery Walker</h2>
			<div style="display:none;">
				<p><strong>Re: Dinner Tonight</strong></p>
				<p>Sure, let's plan on meeting at Highland Kitchen at 8:00 tonight. Can't wait!</p>
			</div>
		</a>
		<a href="#">Delete</a>
	</li>
</ul>

The CSS for the “iconFloat” class:

.iconFloat {
	float: left;
	margin-top: 0px;
	margin-bottom: 0px;
	border-top-width: 0px;
	border-bottom-width: 0px;
	border-left-width: 0px;
	border-right-width: 0px;
	top: 2px; //this will vary depending on application
	padding-bottom: 0px;
}

Now we have a collapsible split list with a plus icon to indicate that there’s more content and a delete icon, but when you click on the list item, it expands but doesn’t turn into a minus icon like would logically be expected. To fix that, we need to add one more line to our javascript code above:
The Javascript (changes highlighted):

$(document).on("click","ul a", function(){
    $(this).children("div:last").toggle(200);
    if($(this).attr("title")) {
        console.log("clicked delete");
    } else {
        //Clicked the link to toggle the content
    }
	$(this).children("div:first").toggleClass("ui-icon-plus ui-icon-minus");
});

That is why we wanted to toggle the LAST div: because there would be 1 more div before it that would contain the collapsible icon (the FIRST div). The .toggleClass() function just adds / removes the specified classes (if the class is there, it removes it; if it isn’t, it adds it).
 
Combine it all together and you should have something like this:

Full HTML/CSS/Javascript

The full HTML/CSS/Javascript source used in the example above:

<html>
	<head>
		<link rel="stylesheet" href="http://code.jquery.com/mobile/1.4.2/jquery.mobile-1.4.2.min.css" />
		<style>
			.iconFloat {
				float: left;
				margin-top: 0px;
				margin-bottom: 0px;
				border-top-width: 0px;
				border-bottom-width: 0px;
				border-left-width: 0px;
				border-right-width: 0px;
				top: 2px;
				padding-bottom: 0px;
			}
		</style>
	</head>
	<body>
		<div data-role="page">
			<div role="main">
				<ul data-role="listview" data-inset="true" data-split-icon="delete">
					<li>
						<a href="#"><div class="iconFloat ui-btn ui-corner-all ui-icon-plus ui-btn-icon-notext"></div>
							<h2>Stephen Weber</h2>
							<div style="display:none;">
								<p><strong>You've been invited to a meeting at Filament Group in Boston, MA</strong></p>
								<p>Hey Stephen, if you're available at 10am tomorrow, we've got a meeting with the jQuery team.</p>
							</div>
						</a>
						<a href="#">Delete</a>
					</li>
					<li>
						<a href="#"><div class="iconFloat ui-btn ui-corner-all ui-icon-plus ui-btn-icon-notext"></div>
							<h2>jQuery Team</h2>
							<div style="display:none;">
								<p><strong>Boston Conference Planning</strong></p>
								<p>In preparation for the upcoming conference in Boston, we need to start gathering a list of sponsors and speakers.</p>
							</div>
						</a>
						<a href="#">Delete</a>
					</li>
					<li>
						<a href="#"><div class="iconFloat ui-btn ui-corner-all ui-icon-plus ui-btn-icon-notext"></div>
							<h2>Avery Walker</h2>
							<div style="display:none;">
								<p><strong>Re: Dinner Tonight</strong></p>
								<p>Sure, let's plan on meeting at Highland Kitchen at 8:00 tonight. Can't wait!</p>
							</div>
						</a>
						<a href="#">Delete</a>
					</li>
				</ul>
			</div>
		</div>
		
		<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
		<script src="http://code.jquery.com/mobile/1.4.2/jquery.mobile-1.4.2.min.js"></script>
		<script lang="text/javascript">
		$(document).on("click","ul a", function(){
			$(this).children("div:last").toggle(200);
			if($(this).attr("title")) {
				console.log("clicked delete");
			} else {
				//Clicked the link to toggle the content
			}
			$(this).children("div:first").toggleClass("ui-icon-plus ui-icon-minus");
		});
		</script>
	</body>
</html>

Conclusion

By including 2 extra div’s per list item, a little custom CSS, and some minor event handling Javascript, we can effectively create a functional collapsible split list. From here, you can easily add function calls to the part of the code for when the user clicks the delete button or choose to do an additional task when the user expands/collapses the content.
 
 
Let me know if you found this tutorial helpful and if you’re using collapsible split lists in the comments below.
 

Could you repeat that?

After a few weeks, working on and off, I’ve almost completely implemented an event calendar that supports repeating events.
 
In my previous update, I mentioned looking at an iCal implementation (which didn’t meet my needs), as well as finding a forum that laid out the groundwork for implementing recurring events in a database. However, I stumbled upon a much simpler solution:
 
A pseudo-cronjob implementation (forum link).
 
From the link above, I would have 3 fields/objects:
 
events (maps an id to the event name, start time, and array of dates)
event-dates (maps a date to an array of id’s)
endlessEvents*
 
*Note: Not yet implemented. Will end up storing a pattern for identifying whether an event falls on a particular day.
 
For events that have a definite end date (either after a number of occurrences or on a specific date), I enumerate all the dates that event would fall on and add the event ID to the specified date(s) in the map of event-dates.
 
If I need to delete an event, I would just need to get the ID and iterate over the map, removing the ID wherever found.
 
If I need to edit an event for a particular day, I would clone the event but give it a different ID, remove the old ID from the event-dates map for that particular day, and add the new event ID.


All that’s left to implement for the calendar is:

  • Endless event pattern matching
  • Editing / Removing an event
  • Modifying the implementation of “ends after x occurrences” to “ends on (date)”

After that, it’s just a short step to implement a checklist displaying what events are happening today. Then I get to play with PhoneGap to try and convert it to an Android / iPhone app.

Side Projects – Update

To update on the side projects I’ve been working on:

Medication Reminder – What was tried:

 
I tried using jQuery iCalendar but couldn’t figure out how to get it to actually display a calendar or link it to a calendar (it appears it’s only for converting an event into an iCal format) so I decided not to go down that route.

Medication Reminder – What has worked:

 
I’ve had a large amount of success using jQueryUI’s Datepicker (now rolled into jQueryMobile 1.4). Since the intended platforms are mobile devices and intended use is for medication reminders, for those that take a lot of medications, having individual visible events on a calendar would either extend the cells of the calendar or overflow on top of the days below it or be cutoff all together resulting in not all the “advertised” information being displayed, I opted to just color the cell in the calendar if there was SOMETHING for that day. Then, when the user click on that day, a window pops up detailing what all is to be taken that day, and if in the past, when the medication was actually taken.
 
By using the “beforeShowDay” option, I can check to see if there are any medications to be taken that day and set the highlight class as necessary. I also realized that since the class name the beforeShowDay option expects is just a string, I could have multiple classes like: “highlight big-date” (trivially) to highlight the date on the calendar and make the date number really big.
 
With this, I got the idea to include the date itself as a “class” so I could quickly pick the EXACT DOM element for a specific date and perform other DOM manipulations using jQuery (e.g.: if mobile device, do nothing special, but if desktop browser / tablet / sufficiently large display, inject medication name / time).
 
With the “medication-event” rendering problem solved (mostly), I’m now focusing on how to store the “medication-events” in a (relatively) small and robust manner (similar to iCal), specifically as it relates to recurring events. I’ve drawn a lot of inspiration from Google Calendar as far as how to present the input of a “medication-event” (“if it’s not broken, don’t fix it” and “don’t reinvent the wheel” come to mind) and from this thread on webdeveloper.com on how to store the “medication-event”. It has also provided some insight on how to render the recurring events as well as how to handle exceptions (e.g.: take daily at 7 am, except on weekends take at 9 am).
 
One other feature I implemented at the last minute to the “Add event” form is adding an auto-complete functionality for the medication name input. Not knowing what’s “common” prescribed medications, I just took a list from needymeds.org and modified the auto-complete to display the suggestions in a scrollable window (to prevent it from “breaking” the flow of the input page). An added benefit is that the brand names are included so the user can type either the generic / chemical name or the brand name and still find the medication (hopefully).

Medication Reminder – What remains:

 
Once the “medication-event” handling is complete, I’m ready to merge the calendar and the easy “one-touch taken medication” features and move on to packaging the web app in Phonegap to be able to leverage the native notification / popup features of Android/iOS.
 
You can view the event calendar at: http://beta.bradleymize.com/cal/
You can view the “one-touch taken medication” feature at: http://beta.bradleymize.com/pills/

Magic: The Gathering Web App

Development Status: Stable – Beta

Click here for the Change Log.

Click here for a list of planned features / work remaining.

Click here to go to the web app.

Main Features:

  • Pure HTML5 / CSS3 / Javascript web application (compatible with computers and most smartphones)
  • Life, Poison, Commander Damage counters
  • Card Search
  • Deck builder and analyzer
  • Coin / Dice simulators
  • Commonly asked rules and comprehensive rules

Development Notes:

I decided to abandon my idea of an “offline” magic the gathering app because of the effort involved with updating the list of cards every expansion and re-compiling the app. It’s very time consuming for just one person to do, and that’s not even getting into managing two separate apps in two different languages (Android Java and Objective-C).
 
Instead I went with a web app because most smartphones have modern browsers that support HTML5 / CSS3 / Javascript, so I could write the app once and it’ll run on just about any computer / phone. The only thing I would need to change (once written) are the stylesheets for difference size screens.
 
I ended up using jQuery, jQuery Mobile, Underscore.js, and Backbone.js. I never used any of those before but luckily jQuery/jQuery Mobile is very well documented with examples. I was quickly able to build the structure of a multipage web app with basic transitions and headers / footers that behaved like drawers.
 
To make sure I didn’t bite off more than I could chew, I started with what I knew first: basic AJAX. I first implemented the card lookup with difficulty using jQuery Mobile’s Autocomplete widget (after realizing mtgapi.com wasn’t returning proper JSONP responses but instead text/html). Then I implemented the Comprehensive Rules search (with the help of a jQuery highlighting plugin). I optimized both to institute a delay before sending the AJAX requests (to minimize the total number of requests). Also implemented a swipe to reveal previous / next card in the card lookup page and added persistent highlights to easily see which card was selected in the list.
 
Then I implemented the random generators: coin flip, 6-sided die, 20-sided die and a custom range roll (-100 to 100).
 
Next I decided to tackle the Life Tracker portion using Underscore + Backbone. I used JSFiddle to prototype the Underscore Template and Backbone View, and when I had something I liked, I integrated it into my app. Both took longer than expected. I implemented persistence using HTML5’s Local Storage. I used JSFiddle again to develop a proof-of-concept for a method to assign damage (drag from one player to another, being able to determine the starting and ending points).