This tutorial walks you through the creation of a Bible viewer as an example of loading content from XML and
dynamically generating pages with jQuery Mobile.
XML Format?
The viewer works with OSIS or Zefania XML format. I selected the KJV translation for this demo, however it should work for any other translation or language.The XML file is 5.5MB, so I've split each Bible book in its own file that will be loaded when needed. Now, largest XML file is ~300KB.
* Check OSIS XML format of the Genesis book after being split.
Multi-page Template
We will use a single HTML file that contains 2 jQuery mobile pages, Home page that will contain a list view of Bible books and another empty page to be used for displaying book chapters.The mobile page is just a DIV element with a data-role of "page". Each page element will have a unique ID (id="foo") to be linked internally like (href='#foo').
The HTML page references to jQuery, jQuery Mobile and the mobile theme CSS in the head like this.
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Holy Bible - Mobile Viewer </title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="http://code.jquery.com/mobile/1.1.0/jquery.mobile-1.1.0.min.css" rel="stylesheet"/>
<script src="http://code.jquery.com/jquery-1.7.1.min.js" type='text/javascript'></script>
<script src="http://code.jquery.com/mobile/1.1.0/jquery.mobile-1.1.0.min.js" type='text/javascript'></script>
.....
This is the mobile-page for home:
<div data-role="page" id="home">
<div data-role="header" data-theme="b">
<h1>Holy Bible - Mobile Viewer</h1>
</div>
<div data-role="content">
<ul id="book_list" data-role="listview"></ul>
</div>
</div>
* The page contains an empty unordered list that will be populated with books names and transformed into the books list view.
And this is the mobile-page for chapters:
<div data-role="page" id="chapter">
<div data-role="header" data-position="fixed" data-theme="b">
<a href="#home" data-role="button" data-icon="home" data-iconpos="notext">Home</a>
<h1></h1>
<a href="" data-role="button" class="next" data-icon="forward" data-iconpos="notext">Next</a>
</div>
<div data-role="content"></div>
<div data-role="footer" data-theme="d">
<span class="ui-title" />
<label for="chapter_num">Jump to chapter</label>
<select name="chapter_num" id="chapter_num" data-mini="true"></select>
</div>
</div>
- Header contains a link to return to home page ('#home') and a 'Next' link with empty 'href' that will be set when dynamically creating chapter pages.
- Content DIV is empty..
- An empty select box in the footer that will have numbers of book chapters to jump to.
Books Array
Will keep books list in an associative array, where the book short-name (abbreviation) is the key -prefixed with underscore. When the user asks for some book for the first time, we will load the XML document of that book and keep it in this array for later requests.
var books = {
_Gen: { bname: 'Genesis' },
_Exod: { bname: 'Exodus' },
_Lev: { bname: 'Leviticus' },
_Num: { bname: 'Numbers' },
_Deut: { bname: 'Deuteronomy' },
_Josh: { bname: 'Joshua' },
_Judg: { bname: 'Judges' },
_Ruth: { bname: 'Ruth' },
_1Sam: { bname: '1 Samuel' },
.......
On Document Ready
We will build the book list on Document-ready event, which will execute once when the single HTML page is loaded.Note that document-ready event is not executed on mobile-pages load as they are dynamically inserted in the DOM.
$(document).ready(function() {
var book_list = $('#book_list');
//
create books as nested list view
var iHtml = '<li>Old Testament <ul>';
for(var x in books) {
if( x=='_Matt') iHtml += '</ul></li> <li>New Testament <ul>';
iHtml += '<li><a href="#chapter?book=' + x.substring(1) + '&num=1">' + books[x].bname + '</a></li>';
};
iHtml += '</ul></li>';
book_list.html( iHtml ).listview('refresh');
});
- * note that we call listview('refresh') after inserting list elements to refresh the list styles.
- Note how link hash is built.
- First part of the hash is '#chapter', which is the ID of chapter mobile-page.
- second part is '?book=...&num=1', which is how we tell the application which Bible book and chapter to display.
Loading Book XML
When a Bible book is requested for the first, will load its XML file with AJAX call. Each XML file is named by the book short-name.// load book xml by its short name
function loadBook(bsname, url, options) {
// show loading icon
$.mobile.showPageLoadingMsg();
// load book xml file and call showChapter again
$.ajax({
url:'books/'+ bsname +'.xml'
,datatype:'xml'
,success:function(xml){
$.mobile.hidePageLoadingMsg();
//
save xml document as a property of the array element
var book = books[ '_' + bsname ];
book['xml'] = xml;
// call showChapter
showChapter( url, options );
}
,error: function(jqXHR, textStatus, errorThrown) {
alert('Error loading book, try again!');
}
})
};
- Calls 'showPageLoadingMsg' before AJAX call to show the loading icon, and calls 'hidePageLoadingMsg' when done.
- XML document returned is saved as a new property of the book array element.
Generating Pages
To generate pages, will need to listen to 'pagebeforechange' event as it gives us a hook to check the URL is being requested.The event handler checks the URL if it starts with '#chapter', then it calls 'showChapter' function.
$(document).bind( "pagebeforechange", function( e, data ) {
// only handle changePage() when loading a page by URL.
if ( typeof data.toPage === "string" ) {
// Handle URLs that request chapter page
var url = $.mobile.path.parseUrl( data.toPage ), regex = /^#chapter/;
if ( url.hash.search(regex) !== -1 ) {
showChapter( url, data.options );
// tell changePage() we've handled this
e.preventDefault()
}
}
});
And this is 'showChapter' function, which dynamically creates the chapter page.
function showChapter( url, options ) {
// parse params in url hash
var params = hashParams(url.hash);
// Get book by
its short-name
var book = books[ '_' + params['book'] ];
if( !book ) {
alert('Book not found!');
return
};
// book xml was not loaded ?
if( book['xml']==undefined || !book['xml'] ){
// call loadBook first
loadBook( params['book'], url, options);
return
};
// get chapter num
var chapterNum = parseInt( params['num'] );
if ( isNaN(chapterNum) || chapterNum<=0 ) chapterNum=1;
//
get chapters nodes in book, chapterNodeName='chapter' for OSIS
XML
var chapters = $( chapterNodeName , book['xml']);
var chaptersSize = chapters.size();
// Use last chapter if num is greater than
chapters count
if( chapterNum > chaptersSize ) chapterNum= chaptersSize;
// get chapter
var chapter = chapters.eq( chapterNum-1 );
//
append verses as paragraphs
var chapterHTML = '';
$( verseNodeName , chapter).each(function(i) {
var vers = $(this);
chapterHTML += '<p><sup>'+ (i+1) +'</sup> '+ vers.text() +'</p>'
});
// Get the empty page we are going to insert our content into.
var $page = $('#chapter');
// Get the header for the page to set it
$header = $page.children( ":jqmData(role=header)" );
$header.find( "h1" ).html( book.bname +' '+ chapterNum );
// Get the content element for the page to set it
$content = $page.children( ":jqmData(role=content)" );
$content.html(chapterHTML);
// change href of next links to the next chapter
var nextChapterNum = chapterNum >= chaptersSize? chaptersSize : chapterNum+1;
$('a.next', $page).attr('href' , '#chapter?book='+ params['book'] + '&num='+ nextChapterNum );
// update data-url that shows up in the browser location
options.dataUrl = url.href;
// switch to the page we just modified.
$.mobile.changePage( $page, options )
};
This function works as follows
- Parses URL params to know the requested book and chapter.
- Checks if the book short-name exists or not.
- Checks that book XML was loaded. If not, it call 'loadBook' and exits the function. And when book XML is loaded, this function will be called again.
- Finds chapters nodes in XML, and checks that chapter number is valid.
- Get requested chapter, and loops its verses nodes to create the corresponding HTML.
- Get chapter page object, sets it heading and content.
- Sets the href of next-button with a link to the next chapter.
- Sets the options.dataUrl to change URL shown in browser location bar.
- Now call changePage() and tell it to switch to the page we just modified.
Great! But in JSON, instead of XML, would be better.
Thanks, why JSON?
thanks, great tutorial, is it possible to add a search bar where one can go directly to the verse they want to vie? also can one change the color scheme?
Thanks @Tom,
Sure we can do a "Jump to verse" box, where you can go directly to selected verse. first, Will need to use the verse number as an ID attribute for each verse paragraph..
To change colors, Check jQuery Mobile themes.
Thanx mike i hope you'll post an example of that "jump to verse" box.
Would it be possible to have an (on screen) page break after each verse? Not a printed page break, this is for display on mobile device. (Obviously not using for Bible, or there'd be a zillion pages...lol). But, I'm making a book and all my 'chapter' copy is showing up on one page for each section.
For instance, I edited the Gen.xml file to be Section 1, and made each 'verse' a chapter, Lev.xml is Section 2, Exod.xml is Section 3. Each section contains 5 chapters and I want them to display on a sep page instead of all 5 on one. Does that make sense?
I'm a newbie (obviously) so looking for any help or input/ideas...thanks in advance!
Any ideas? Thanks!
How about changing font size dynamically?
Great, I saw it on CrossWire Dev List and i like it so much .
Would be great if it supports commentaries, fast jump, font options that will make it a crosswire frontend that's compatible with all devices :)
Thanks Pola,
this is just a starting point, however I might update it later.
Mike you done a wonderful job with this Bible. I liked the way it is setup with xml files. I would also like to seen a way to go directly to a verse and do a search for a keyword. I would add it but I don't know anything about coding so I guess I will have to wait until you have time to add them.