Introduction
The purpose of this project is to demonstrate how one can create a Single Page Application to store a persons information in various tabs without additional pages. It is to also explorer the datepicker widgets, the slider, horizontal radio buttons and the switch control in JQuery Mobile. This also demonstrates how to load a SelectMenu dynamically from stored data that resides in LocalStorage and also left align the SelectMenu text. For the listing of family members, I demonstrate how to feed count-bubbles with a member's age, and show the contact details on each listview item of the family member. Interesting also is the fact that all family members will always be listed in ascending order no matter how you add them. That is through a script to sort the JSON when a save is performed by the web app.
Here is the complete source code for MyFamily.Show. Download MyFamily.Show.zip
Background
I wanted to find out how I can use navigation tabs on my JQuery Mobile App to split forms that into various information parts. With that I also wanted to know how I can load a SelectMenu dynamically from information stored from my LocalStorage and this information should be related e.g. parents of a person. I wanted the information parts of a person to reside within one page separated by div elements and not be loading from another page.
In developing a Family Tree, basic information about a person is needed. These parts I have concluded to be related to the following items:
- Personal Information (Gender, Full Name, Salutation, ID Number / SSN, Date of Birth, Age, Is Living? and Place of Birth)
- Contact Details - (Cellphone / Mobile phone number, Email Address)
- Relationships - (Father and Mother) and
- Death - (Date of Death, Place of Death)
This app of couse can be customised to have additional information but I wanted to keep it to basic information for this demonstration.
The information captured gets stored in LocalStorage.
Using the code
Like all HTML5 web apps, the user interface should be designed and then the linkage of the various buttons that save, retrieve and delete the information created. We will define a page with four navigation bar items, these being Personal Information, Contact Details, Relationships and Death. Each of these tabs will have information that will be requested from the user and the user saves the details of the family member at the Death screen. I'm still to explore how I can make this into a wizard like interface.
I am following the same approach I have been using in creating the NoteKeeper application for CRUD application development depicted here.
I will delve into the important stuff and you can review the source code for more details. This will be defining the navigation bar and the pages and the source scripts that link them.
The Navigation Bar - html definition and output
<div id="pgAddFamilyMember" data-role="page">
<header id="pgAddFamilyMemberheader" data-role="header" data-position="fixed">
<h1>MyFamily.Show > Add Family Member</h1>
<a data-role="button" id="pgAddFamilyMemberBack" data-icon="arrow-l" class="ui-btn-left">Back</a>
<div id="pgAddFamilyMemberHdrNav" data-role="navbar">
<ul>
<li><a href="#" data-href="pgAddFamilyMemberPersonalInformation" id="pgAddFamilyMemberPersonalInformationBtn" class="ui-btn-active">Personal Information</a>
</li>
<li><a href="#" data-href="pgAddFamilyMemberContactDetails" id="pgAddFamilyMemberContactDetailsBtn">Contact Details</a>
</li>
<li><a href="#" data-href="pgAddFamilyMemberRelationships" id="pgAddFamilyMemberRelationshipsBtn">Relationships</a>
</li>
<li><a href="#" data-href="pgAddFamilyMemberDeath" id="pgAddFamilyMemberDeathBtn">Death</a>
</li>
</ul>
</div>
</header>
The page to add a family member is called pgAddFamilyMember and the one to update called pgEditFamilyMember, these have a similar navigation bar. Each element in the navigation bar has a data-href attribute that stores the name of the div to navigate to when each button is pressed. I had to add an id attribute to each button because each time this page is added, I wanted it to default to the first screen, i.e. Personal Details, irrespective of where the user was last time. This is achieved by firing a click event of the header button using the id. That trigger is part of this code below, that gets fired before a page is shown.
The resulting output of the header from above is depicted in Figure 1 below, running from an iPad Ripple emulator.
Figure 1
$(document).on('pagebeforechange', function(e, data){
var toPage = data.toPage[0].id;
switch (toPage) {
case 'pgFamilyMember':
app.checkForFamilyMemberStorage();
break;
case 'pgEditFamilyMember':
pgEditFamilyMemberClear();
app.pgEditFamilyMemberLoadFather();
app.pgEditFamilyMemberLoadMother();
$('#pgEditFamilyMemberPersonalInformationBtn').trigger('click');
break;
case 'pgAddFamilyMember':
pgAddFamilyMemberClear();
app.pgAddFamilyMemberLoadFather();
app.pgAddFamilyMemberLoadMother();
$('#pgAddFamilyMemberPersonalInformationBtn').trigger('click');
break;
default:
}
});
What the code above basically does is:
1. Before the listing of the family members, check if there are any saved in Local Storage and update a listview
2. Before the screen to edit family members is shows, clear the contents of all the controls in that screen, loady the father and mother selectmenus with names that are already stored in the family tree records and then
3. Trigger a click event to show the first screen, i.e. being Personal Information.
The code to make the navigation to work with the divs sits in the attached navbar.js file and this file should be included before the jquery mobile.js script call.
$(document).delegate('.ui-navbar ul li > a', 'click', function() {
$(this).closest('.ui-navbar').find('a').removeClass('ui-btn-active');
$(this).addClass('ui-btn-active');
$('#' + $(this).attr('data-href')).show().siblings('.tab-content').hide();
return false;
});
For each item in the navigation bar's list, when its selected, remove the active indicator of that button, i.e. the last active button, then add an active indicator to the selected button. After that read the data-href attribute and show the linked div and hide everything else. Each div has a class of tab-content defined in it as will be shown below and a style was added to the source to ensure this all works.
The style added to make this all work, i.e. navigation without moving from page to page is:
.tab-content {display:none;}
.tab-content:first-child { display: block;}
We have explained the Navigation Bar, now lets deal with the various information parts.
Personal Information - html definition and output.
Figure 2 below depicts the output of the Personal Details screen as defined in the fields mentioned above.
Figure 2
<div id="pgAddFamilyMemberPersonalInformation" class="tab-content">
<div data-role="fieldcontain">
<fieldset id="fspgAddFamilyMemberGender" data-role="controlgroup" data-type="horizontal" data-mini="true">
<legend>Gender<span style='color:red;'>*</span></legend>
<input type="radio" name="pgAddFamilyMemberGender" id="pgAddFamilyMemberGendermale" autocomplete="off" title="" value="male" required></input>
<label for="pgAddFamilyMemberGendermale" id="lblpgAddFamilyMemberGendermale">Male<span style='color:red;'>*</span></label>
<input type="radio" name="pgAddFamilyMemberGender" id="pgAddFamilyMemberGenderfemale" autocomplete="off" value="female"></input>
<label for="pgAddFamilyMemberGenderfemale" id="lblpgAddFamilyMemberGenderfemale">Female</label>
</fieldset>
</div>
<div data-role="fieldcontain">
<label for="pgAddFamilyMemberFullName" id="lblpgAddFamilyMemberFullName">Full Name<span style='color:red;'>*</span></label>
<input type="text" name="pgAddFamilyMemberFullName" id="pgAddFamilyMemberFullName" placeholder="Enter full name here." autocomplete="off" data-clear-btn="true" title="Enter full name here." required></input>
</div>
<div data-role="fieldcontain">
<fieldset id="fspgAddFamilyMemberSalutation" data-role="controlgroup" data-type="horizontal" data-mini="true">
<legend>Salutation<span style='color:red;'>*</span></legend>
<input type="radio" name="pgAddFamilyMemberSalutation" id="pgAddFamilyMemberSalutationMr" autocomplete="off" value="Mr"></input>
<label for="pgAddFamilyMemberSalutationMr" id="lblpgAddFamilyMemberSalutationMr">Mr</label>
<input type="radio" name="pgAddFamilyMemberSalutation" id="pgAddFamilyMemberSalutationMs" autocomplete="off" value="Ms"></input>
<label for="pgAddFamilyMemberSalutationMs" id="lblpgAddFamilyMemberSalutationMs">Ms</label>
<input type="radio" name="pgAddFamilyMemberSalutation" id="pgAddFamilyMemberSalutationMrs" autocomplete="off" value="Mrs"></input>
<label for="pgAddFamilyMemberSalutationMrs" id="lblpgAddFamilyMemberSalutationMrs">Mrs</label>
<input type="radio" name="pgAddFamilyMemberSalutation" id="pgAddFamilyMemberSalutationDr" autocomplete="off" value="Dr"></input>
<label for="pgAddFamilyMemberSalutationDr" id="lblpgAddFamilyMemberSalutationDr">Dr</label>
<input type="radio" name="pgAddFamilyMemberSalutation" id="pgAddFamilyMemberSalutationProf" autocomplete="off" value="Prof"></input>
<label for="pgAddFamilyMemberSalutationProf" id="lblpgAddFamilyMemberSalutationProf">Prof</label>
</fieldset>
</div>
<div data-role="fieldcontain">
<label for="pgAddFamilyMemberIDNumber" id="lblpgAddFamilyMemberIDNumber">ID Number<span style='color:red;'>*</span></label>
<input type="number" name="pgAddFamilyMemberIDNumber" id="pgAddFamilyMemberIDNumber" placeholder="Enter id number here." autocomplete="off" data-clear-btn="true" title="Enter id number here." required></input>
</div>
<div data-role="fieldcontain">
<label for="pgAddFamilyMemberDateofBirth" id="lblpgAddFamilyMemberDateofBirth">Date of Birth<span style='color:red;'>*</span></label>
<input data-options='{"mode":"flipbox","dateFormat":"%Y-%m-%d","overrideDateFormat":"%Y-%m-%d"}' type="text" name="pgAddFamilyMemberDateofBirth" id="pgAddFamilyMemberDateofBirth" placeholder="Enter date of birth here." autocomplete="off" data-role="datebox" title="Enter date of birth here." required></input>
</div>
<div data-role="fieldcontain">
<label for="pgAddFamilyMemberAge" id="lblpgAddFamilyMemberAge">Age<span style='color:red;'>*</span></label>
<input min="0" max="200" type="range" name="pgAddFamilyMemberAge" id="pgAddFamilyMemberAge" autocomplete="off" title="" required></input>
</div>
<div data-role="fieldcontain">
<label for="pgAddFamilyMemberLiving" id="lblpgAddFamilyMemberLiving">Is Living<span style='color:red;'>*</span></label>
<select name="pgAddFamilyMemberLiving" id="pgAddFamilyMemberLiving" data-role="slider" data-mini="true" dir="ltr" class="required">
<option value="no">No</option>
<option value="yes">Yes</option>
</select>
</div>
<div data-role="fieldcontain">
<label for="pgAddFamilyMemberPlaceofBirth" id="lblpgAddFamilyMemberPlaceofBirth">Place of Birth<span style='color:red;'>*</span></label>
<input type="text" name="pgAddFamilyMemberPlaceofBirth" id="pgAddFamilyMemberPlaceofBirth" placeholder="Enter place of birth here." autocomplete="off" data-clear-btn="true" title="Enter place of birth here." required></input>
</div>
</div>
The above script defines the html behind the Personal Information screen. As you can see, the first line of the script has a class called 'tab-content' to tell the app that this will be treated within the confined of a tab, this ensures that our navigation is in sync.
Contact Details - html definition and output
The contact details screen is not as complex, only the email and cellphone details are requested.
<div id="pgAddFamilyMemberContactDetails" class="tab-content">
<div data-role="fieldcontain">
<label for="pgAddFamilyMemberCellphone" id="lblpgAddFamilyMemberCellphone">Cellphone</label>
<input type="number" name="pgAddFamilyMemberCellphone" id="pgAddFamilyMemberCellphone" placeholder="Enter cellphone here." autocomplete="off" data-clear-btn="true"></input>
</div>
<div data-role="fieldcontain">
<label for="pgAddFamilyMemberEmail" id="lblpgAddFamilyMemberEmail">Email</label>
<input type="email" name="pgAddFamilyMemberEmail" id="pgAddFamilyMemberEmail" placeholder="Enter email here." autocomplete="off" data-clear-btn="true"></input>
</div>
</div>
<div id="pgAddFamilyMemberRelationships" class="tab-content">
<div dir="ltr" data-role="fieldcontain">
<label for="pgAddFamilyMemberFather" id="lblpgAddFamilyMemberFather">Father</label>
<select name="pgAddFamilyMemberFather" id="pgAddFamilyMemberFather" data-native-menu="false" data-mini="true" data-inline="true" dir="ltr">
<option value="null" data-placeholder="true">Select Father</option>
<option value="Father1">Father1</option>
<option value="Father2">Father2</option>
<option value="Father3">Father3</option>
</select>
</div>
<div dir="ltr" data-role="fieldcontain">
<label for="pgAddFamilyMemberMother" id="lblpgAddFamilyMemberMother">Mother</label>
<select name="pgAddFamilyMemberMother" id="pgAddFamilyMemberMother" data-native-menu="false" data-mini="true" data-inline="true" dir="ltr">
<option value="null" data-placeholder="true">Select Mother</option>
<option ></option>
</select>
</div>
</div>
You will notice that the SelectMenu control for Father has some options loaded by default when the app starts. However when you run the app, these Father1,Father2 and Father3 options will never be listed at all because the Father selectmenu will be loaded dynamically with existing Full Names from our storage. Figure 3 below is the resulting output of the Contact Details Definition.
Figure 3
Relationships - html definition and output
Figure 4 below asks for the Father and Mother's full names. These names are loaded from existing family members that have been captured on MyFamily.Show web app as stored from LocalStorage.
Figure 4:
As you will note, these DropDown lists don't fill the whol screen like they normally do. This is because a few tricks were applied to ensure that happens and I will explain these below.
Death- html definition and output
In case one of the family members has passed on, you can also capture their date of death and where they died. This is just to keep a family history going on. The definition of this screen/div is also simple and not fancy, as depicted in Figure 5 below.
<div id="pgAddFamilyMemberDeath" class="tab-content">
<div data-role="fieldcontain">
<label for="pgAddFamilyMemberDateofDeath" id="lblpgAddFamilyMemberDateofDeath">Date of Death</label>
<input data-options='{"mode":"flipbox","dateFormat":"%Y-%m-%d","overrideDateFormat":"%Y-%m-%d"}' type="text" name="pgAddFamilyMemberDateofDeath" id="pgAddFamilyMemberDateofDeath" placeholder="Enter date of death here." autocomplete="off" data-role="datebox"></input>
</div>
<div data-role="fieldcontain">
<label for="pgAddFamilyMemberPlaceofDeath" id="lblpgAddFamilyMemberPlaceofDeath">Place of Death</label>
<input type="text" name="pgAddFamilyMemberPlaceofDeath" id="pgAddFamilyMemberPlaceofDeath" placeholder="Enter place of death here." autocomplete="off" data-clear-btn="true"></input>
</div>
<div><button type="submit" id="pgAddFamilyMemberSave" class="ui-btn ui-corner-all ui-shadow ui-btn-b">Save Family Member</button>
</div>
</div>
</div>
Figure 5
I have decided to have the Update Family Member button on the Death div as the user must enter all the information in the tabs, where available and then save. I will explorer a Back, Next navigation approach next time to make this seamless.
The Family Member Edit/Update Screen
The only difference between the Add Member / Update Member screen are just the control names. I did not want to use the same screen for Add/Update functions as such at times proves a challenge to maintain, yes, its doable, but for the sake of code maintenance, Ive chosen not to follow that approach. As you might have noted by now, all Add related controls are prefixed by pgAdd and all edit controls prefixed by pgEdit. That's the same approach that I followed even with the NoteKeeper web app.
Listing Family Members
The approach to list family members has taken the listview further than my previous posts to now include a count bubble, and contact details.
<div id="pgFamilyMembercontent" data-role="content">
<ul data-role="listview" data-inset="true" id="pgFamilyMemberList" data-autodividers="true" data-filter="true" data-filter-placeholder="Search Family Members" data-filter-reveal="false">
<li data-role="list-divider">FamilyMemberHdr</li>
<li id="noFamilyMember">You have no family members</li>
</ul>
</div>
Whilst the family listing is defined here, it depends on various scripts within the web app. These are depicted below.
var FamilyMemberLi = '<li ><a href="#pgEditFamilyMember?FullName=Z2"><h2>Z1</h2><p>DESCRIPTION</p><span class="ui-li-count">COUNTBUBBLE</span></a></li>';
var FamilyMemberHdr = '<li data-role="list-divider">FamilyMemberHdr</li>';
var noFamilyMember = '<li id="noFamilyMember">You have no family members</li>';
app.displayFamilyMember = function(){
var FamilyMemberObj = app.getFamilyMember();
var html = '';
var n;
for (n in FamilyMemberObj){
var FamilyMemberRec = FamilyMemberObj[n];
var nItem = FamilyMemberLi;
nItem = nItem.replace(/Z2/g,n);
var nTitle = '';
nTitle = n.replace(/-/g,' ');
nItem = nItem.replace(/Z1/g,nTitle);
var nCountBubble = '';
nCountBubble += FamilyMemberRec.Age;
nItem = nItem.replace(/COUNTBUBBLE/g,nCountBubble);
var nDescription = '';
nDescription += FamilyMemberRec.Cellphone;
nDescription += ', ';
nDescription += FamilyMemberRec.Email;
nItem = nItem.replace(/DESCRIPTION/g,nDescription);
html += nItem;
}
$('#pgFamilyMemberList').html(FamilyMemberHdr + html).listview('refresh');
};
Resulting in something more along these lines, as depicted in Figure 6.
Figure 6
You will notice that the family members when adding them, always display in ascending order when being listed. Selecting each family member of course opens up the Update Family Member screen. This sorting of the saved family members happens when the records are saved in LocalStorage.
This is performed through this little code as added to the save methods.
Sorting the Family Member Details JSON
FamilyMemberObj[FullName] = FamilyMemberRec;
var keys = Object.keys(FamilyMemberObj);
keys.sort();
var sortedObject = Object();
var i;
for (i in keys) {
key = keys[i];
sortedObject[key] = FamilyMemberObj[key];
}
FamilyMemberObj = sortedObject;
localStorage['myfamilyshow-familymember'] = JSON.stringify(FamilyMemberObj);
The speed of this small code script is a variable perhaps it could be optimized in some way, I have a feeling that for very lardge datasets, one would have to display a progressbar to show something happening on the screen. I will explorer progress bars in follow up posts. The code above gets all the keys of the JSON after a family members details have been saved. The primary key for each family member is the Full Name. For family members that share the same name, perhaps we can add I, II, III, suffixes to the names as the web app will over-write an existing member. The sortedObject is then assigned to the object to stringify and save to localstorage.
Loading Fathers and Mothers to SelectMenu
Besides the single page navigation I wanted to add, loading SelectMenus dynamically was a very interesting task I wanted to do. My thoughts were not just about this, but about other web apps that will have relationships between the various models and I would need to add for example a Category on Products screen. I needed something generic that I can easily customise easily for my RAD tool.
Three motheds basically achieve the SelectMenu updates. We want fathers and mothers, thus we need to get all full names first from all stored records, we defined that function. Note that the function name is the ModelName and FieldName combination. Thus for each model that you might want to use, such a combination might suffice.
app.getFamilyMemberFullName = function(){
var FamilyMemberObj = app.getFamilyMember();
var n;
var dsFields = [];
for (n in FamilyMemberObj){
var FamilyMemberRec = FamilyMemberObj[n];
var dsField = FamilyMemberRec.FullName;
dsFields.push(dsField);
}
return dsFields;
};
1. This gets all family members and assigns that to a FamilyMemberObj.
2. We loop through each family member record and get the FullName and push these full names to an array.
The next function then parses this array and updates the specific Select Menu, before the page is shown as explained above.
app.pgAddFamilyMemberLoadFather = function(){
var FamilyMemberObj = app.getFamilyMemberFullName();
var dsdf;
$('#pgAddFamilyMemberFather').empty();
$('#pgAddFamilyMemberFather').selectmenu('refresh');
var options = [];
options.push('<option value="null" data-placeholder="true">Select Father</option>');
for (dsdf in FamilyMemberObj){
var Father = FamilyMemberObj[dsdf];
options.push("<option value='" + Father + "'>" + Father + "</option>");
}
$('#pgAddFamilyMemberFather').append(options.join("")).selectmenu();
$('#pgAddFamilyMemberFather').selectmenu('refresh');
};
The app.getFamilyMemberFullName calls a getFamily member method which basically reads information thats stored in this format from LocalStorage, for each member.
{"Anele-Mbanga":{"Image":"","Gender":"male","FullName":"Anele Mbanga","Salutation":"Mr","IDNumber":
"7304155526089","DateofBirth":"2015-03-02","Age":"42","Living":"yes","PlaceofBirth":"East London",
"Father":"null","Mother":"null","Cellphone":"0817366739","Email":"anele@mbangas.com",
"DateofDeath":"","PlaceofDeath":""},"Esona":{"Image":"","Gender":"female","FullName":
"Esona","Salutation":"Ms","IDNumber":"20100401","DateofBirth":"2010-04-02","Age":"5",
"Living":"yes","PlaceofBirth":"Johannesburg","Father":"Anele Mbanga","Mother":"null",
"Cellphone":"","Email":"esona@mbangas.com","DateofDeath":"","PlaceofDeath":""},
"Olothando-Mbanga":{"Image":"","Gender":"female","FullName":"Olothando Mbanga",
"Salutation":"Ms","IDNumber":"","DateofBirth":"2015-03-02","Age":"6","Living":"no",
"PlaceofBirth":"Johannesburg","Father":"Anele Mbanga","Mother":null,"Cellphone":"",
"Email":"olothando@mbangas.com","DateofDeath":"","PlaceofDeath":""},
"Usibabale-Mbanga":{"Image":"","Gender":"male","FullName":"Usibabale Mbanga",
"Salutation":"Mr","IDNumber":"","DateofBirth":"2015-03-02","Age":"12",
"Living":"yes","PlaceofBirth":"Johannesburg","Father":"Anele Mbanga","Mother":"null",
"Cellphone":"","Email":"usibabale@mbangas.com","DateofDeath":"","PlaceofDeath":""}}
This being stored in LocalStorage key:
myfamilyshow-familymember
myfamilyshow being the name of the web app and familymember being the name of the model we are storing information for.
Points of Interest
From this I learned that I needed to clear my controls before additions or edits to ensure that everything is reset. I also discovered how to use a navigation bar buttons without navigating from one page to another. Setting values for the Radio Group proved challenging and I ended up setting the actual value on the code by using scripts like
var opts = 'pgEditFamilyMemberGender' + FamilyMemberRec.Gender;
$('#' + opts).prop('checked', true);
$('#' + opts).checkboxradio('refresh');
$('#pgEditFamilyMemberFullName').val(FamilyMemberRec.FullName);
For example, this will set a control named pgEditFamilyMemberGendermale to true as setting the attribute using the name attribute just would not work properly for me. The unfortunate part is that information is still scattered in the web about how to do these simple coding exercises. I've head to check most methods I found, purported to work which did not for me to end up with a solution. Due to my not so much experience with JQuery and its API, producing this has been a trial and error exercise. I however am working on a RAD tool I call JQM.Show to automate most of this development in three easy steps.
The other find was the datepicker widget that shows a date in this format as depicted in Figure 7 when a user selects a date. You can check the source code of the js that has the control and visit their site for how you can customise it.
History
The basis of these articles originate from my article with the NoteKeeper. Compared to that, the sorting algorithim to sort the JSON before it being saved has been an eye opener. Also being able to customize the listview depending on options I choose has been a marvel.