www.it-ebooks.info
For your convenience Apress has placed some of the front
matter material after the index. Please use the Bookmarks
and Contents at a Glance links to access them.
www.it-ebooks.info
Contents at a Glance
AbouttheAuthor................................................................................................... xii
AbouttheTechnicalReviewer ............................................................................. xiii
Acknowledgments ............................................................................................... xiv
Chapter1:GettingReady ........................................................................................1
Chapter2:GettingStarted ....................................................................................15
Chapter3:AddingaViewModel...........................................................................47
Chapter4:UsingURLRouting ...............................................................................77
Chapter5:CreatingOfflineWebApps.................................................................109
Chapter6:StoringDataintheBrowser ..............................................................137
Chapter7:CreatingResponsiveWebApps.........................................................169
Chapter8:CreatingMobileWebApps ................................................................195
Chapter9:WritingBetterJavaScript..................................................................229
Index ...................................................................................................................261
iv
www.it-ebooks.info
CHAPTER 1
Getting Ready
Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver-sidecoding.Thisstarted
becausebrowsersandthedevicestheyrunonhavebeenlesscapablethanenterprise-classservers.To
provideanykindofseriouswebappfunctionality,theserverhadtodoalloftheheavyliftingforthe
browsers,whichwasprettydumbandsimplebycomparison.
Overthelastfewyears,browsershavegotsmarter,morecapable,andmoreconsistentinhowthey
implementwebtechnologyandstandards.Whatusedtobeafighttocreateuniquefeatureshasbecome
abattletocreatethefastestandmostcompliantbrowser.Theproliferationofsmartphonesandtablets
hascreatedahugemarketforhigh-qualitywebapps,andthegradualadoptionofHTML5providesweb
applicationdeveloperswithasolidfoundationforbuildingrichandfluidclient-sideexperiences.
Sadly,whiletheclient-sidetechnologyhascaughtupwiththeserverside,thetechniquesthat
client-sideprogrammersusestilllagbehind.Thecomplexityofclient-sidewebappshasreacheda
tippingpointwherescale,elegance,andmaintainabilityareessentialandthedaysofhackingouta
quicksolutionhavepassed.Inthisbook,Ileveltheplayingfield,showingyouhowtostepupyour
client-sidedevelopmenttoembracethebesttechniquesfromtheserver-sideworldandcombinethem
withthelatestHTML5features.
AboutThisBook
Thisismy15thbookabouttechnology,andtomarkthis,Apressaskedmetodosomethingdifferent:
sharethetools,tricks,andtechniquesthatIusetocreatecomplexclient-sidewebapps.Theresultis
somethingthatismorepersonal,informal,andeclecticthanmyregularwork.Ishowyouhowtotake
industrial-strengthdevelopmentconceptsfromserver-sidedevelopmentandapplythemtothe
browser.Byusingthesetechniques,youcanbuildwebappsthatareeasiertowrite,areeasierto
maintain,andofferbetterandricherfunctionalitytoyourusers.
WhoAreYou?
Youareanexperiencedwebdeveloperwhoseprojectshavestartedtogetoutofcontrol.Thenumberof
bugsinyourJavaScriptcodeisincreasing,andittakeslongertofindandfixeachone.Youaretargeting
anever-widerrangeofdevice,includingdesktops,tablets,andsmartphones,andkeepingitallworking
isgettingtougher.Yourworkingdaysarelonger,butyouhavelesstimetospendonnewfeatures
becausemaintainingthecodeyoualreadyhavesucksupabigchuckofyourtime.
Theexcitementthatcomesfromyourworkhasfaded,andyouhaveforgottenwhatitfeelsliketo
haveareallyproductivedayofcoding.Youknowsomethingiswrong,youknowthatyouarelosingyour
grip,andyouknowyouneedtofindadifferentapproach.Ifthissoundsfamiliar,thenyouaremytarget
reader.
1
www.it-ebooks.info
CHAPTER1GETTINGREADY
WhatDoYouNeedtoKnowBeforeYouReadThisBook?
Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammertounderstandthe
content.YouneedaworkingknowledgeofHTML,youneedtoknowhowtowriteJavaScript,andyou
haveusedbothtocreateclient-sidewebapps.Youwillneedtounderstandhowabrowserworks,how
HTTPfitsintothepicture,andwhatAjaxrequestsareandwhyyoushouldcareaboutthem.
WhatIfYouDon’tHaveThatExperience?
Youmaystillgetsomebenefitfromthisbook,butyouwillhavetofigureoutsomeofthebasicsonyour
own.Ihavewrittenacoupleofotherbooksyoumightfindusefulasprimersforthisone.Ifyouarenew
toHTML,thenreadTheDefinitiveGuidetoHTML5.Thisexplainseverythingyouneedtocreateregular
webcontentandbasicwebapps.IexplainhowtouseHTMLmarkupandCSS3(includingthenew
HTML5elements)andhowtousetheDOMAPIandtheHTML5APIs(includingaJavaScriptprimerif
youarenewtothelanguage).ImakealotofuseofjQueryinthisbook.Iprovidealloftheinformation
youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksandhowitrelatestothe
DOMAPI,thenreadProjQuery.BothofthesebooksarepublishedbyApress.
Booksaside,youcanlearnalotaboutHTMLandthebrowserAPIsbyreadingthespecifications
publishedbytheW3Catwww.w3.org.Thespecificationsareauthoritativebutcanbehard-goingandare
notalwaysthatclear.AmorereadilyaccessibleresourceistheMozillaDeveloperNetworkat
http://developer.mozilla.org.ThisisanexcellentsourceofinformationabouteverythingfromHTML
toJavaScript.ThereisageneralbiastowardFirefox,butthisisn’tusuallyaproblemsincethe
mainstreambrowsersaregenerallycompliantandconsistentinthewaytheyimplementwebstandards.
IsThisaBookAboutHTML5?
No,althoughIdotalkaboutsomeofthenewHTML5JavaScriptAPIs.Mostofthisbookisabout
technique,mostofwhichwillworkwithHTML4justasitdoeswithHTML5.Somechaptersarebuilt
purelyonHTML5APIs(suchasChapters5and6,whichshowyouhowtocreatewebappsthatwork
offlineandhowtostoredatainthebrowser),buttheotherchaptersarenottiedtoanyparticularversion
ofHTML.Idon’tgetintoanydetailaboutthenewelementsdescribedinHTML5.Thisisabookabout
programming,andthenewelementsdon’thavemuchimpactonJavaScriptprogramming.
WhatIstheStructureofThisBook?
InChapter2,IbuildasimplewebappforafictitiouscheeseretailercalledCheeseLux,buildingonthe
basicexampleIintroducelaterinthischapter.Ifollowsomeprettystandardapproachesforcreating
thiswebappandspendtherestofthebookshowingyouhowtoapplyindustrial-strengthtechniquesto
improvedifferentaspects.Ihavetriedtokeepeachchapterreasonablyseparate,butthisisareasonably
informalbook,andIdointroducesomeconceptsgraduallyoveranumberofchapters.Eachchapter
buildsonthetechniquesintroducedinthechaptersthatgobeforeit.Youshouldreadthebookin
chapterorderifyoucan.Thefollowingsectionssummarizethechaptersinthisbook.
Chapter1:GettingReady
Asidefromdescribingthisbook,IintroducethestaticHTMLversionoftheCheeseLuxexample,whichI
usethroughoutthisbook.Ialsolistthesoftwareyouwillneedifyouwanttore-createtheexampleson
yourownorexperimentwiththelistingsthatareincludedinthesourcecodedownloadthat
accompaniesthisbook(andwhichisavailablefreefromApress.com).
2
www.it-ebooks.info
CHAPTER1GETTINGREADY
Chapter2:GettingStarted
Inthischapter,IusesomebasictechniquestocreateamoredynamicversionoftheCheeseLux
example,movingfromawebsitetoawebapp.Iusethisasanopportunitytointroducesomeofthe
toolsandconceptsthatyouwillneedfortherestofthebookandtoprovideacontextsothatIcanshow
bettertechniquesinlaterchapters.
Chapter3:AddingaViewModel
ThefirstadvancedtechniqueIdescribeisintroducingaclient-sideviewmodelintoawebapp.View
modelsareakeycomponentindesignpatternssuchasModelViewController(MVC)andModel-ViewViewModel.Ifyouadoptonlyonetechniquefromthisbook,thenmakeitthisone;itwillhavethe
biggestimpactonyourdevelopmentpractices.
Chapter4:UsingURLRouting
URLroutingallowsyoutoscaleupthenavigationmechanismsinyourwebapps.Youmaynothave
realizedthatyouhaveanavigationproblem,butwhenyouseehowURLroutingcanworkontheclient
side,youwillseejusthowpowerfulandflexibleatechniqueitcanbe.
Chapter5:CreatingOfflineWebApps
Inthischapter,IshowyouhowtousesomeofthenewHTML5JavaScriptAPIstocreatewebappsthat
workevenwhentheuserisoffline.Thisisapowerfultechniquethatisincreasinglyimportantas
smartphonesandtabletsgainmarketpenetration.Theideaofanalways-onnetworkconnectionis
changing,andbeingabletoaccommodateofflineworkingisessentialformanywebapps.
Chapter6:StoringData
Beingabletorunthewebappofflineisn’tmuchuseunlessyoucanalsoaccessstoreddata.Inthis
chapter,IshowyouthedifferentHTML5APIsthatareavailableforstoringdifferentkindsofdata,
rangingfromsimplename/valuepairstosearchablehierarchiesofpersistedJavaScriptobjects.
Chapter7:CreatingResponsiveWebApps
Thereareentirecategoriesofweb-enableddevicesthatfalloutsideofthetraditionaldesktopandmobile
taxonomy.Oneapproachtodealingwiththeproliferationofdifferentdevicetypesistocreatewebapps
thatadaptdynamicallytothecapabilitiesofthedevicetheyarebeingusedon,tailoringtheir
appearance,functionality,andinteractionmodelsasrequired.Inthischapter,Ishowyouhowtodetect
thecapabilitiesyoucareaboutandrespondtothem.
3
www.it-ebooks.info
CHAPTER1GETTINGREADY
Chapter8:CreatingMobileWebApps
Analternativetocreatingresponsivewebappsistocreateaseparateversionthattargetsaspecificrange
ofdevices.Inthischapter,IshowyouhowtousejQueryMobiletocreatesuchawebappandhowto
incorporateadvancedfeaturessuchasURLroutingintoamobilewebapp.
Chapter9:WritingBetterJavaScript
Thelastchapterinthisbookisaboutimprovingyourcode—notintermsofusingJavaScriptbetterbut
intermsofcreatingeasilymaintainedcodemodulesthatareeasiertouseinyourownprojectsand
easiertosharewithothers.Ishowyousomeconvention-basedapproachesandintroducethe
AsynchronousModuleDefinition,whichsolvessomecomplexproblemswhenexternallibrarieshave
dependenciesonotherfunctionality.Ialsoshowyouhowyoucaneasilyapplyunittestingtoyour
client-sidecode,includinghowtounittestcomplexHTMLtransformations.
DoYouDescribeDesignPatterns?
Idon’t.Thisisn’tthatkindofbook.Thisisabookaboutgettingresults,andIdon’tspendalotoftime
discussingthedesignpatternsthatunderpineachtechniqueIdescribe.Ifyouarereadingthisbook,
thenyouwanttoseethoseresultsandgetthebenefitstheyprovidenow.Myadviceistosolveyour
immediateproblemsandthenstartresearchingthetheory.Alotofgoodinformationisavailableabout
designpatternsandtheassociatedtheory.Wikipediaisagoodplacetostart.Somereadersmaybe
surprisedattheideaofWikipediaasasourceofprogramminginformation,butitoffersawealthofwellbalancedandwell-writtencontent.
Ilovedesignpatterns.Ithinktheyareimportantandusefulandavaluablemechanismfor
communicatinggeneralsolutionstocomplexproblems.Sadly,theyarealltoooftenusedasakindof
religion,whereeveryaspectofapatternmustbeappliedexactlyasspecifiedandlongandnasty
conflictsbreakoutaboutthemeritsandapplicabilityofcompetingpatterns.
Myadviceistoconsiderdesignpatternsasthefoundationfordevelopingtechniques.Mixand
matchdifferentdesignpatternstosuityourprojectsandcherry-pickthebitsthatsolvetheproblemsyou
face.Don’tletanyonedictatethewaythatyouusepatterns,andalwaysremainfocusedonfixingreal
problemsinrealprojectsforrealusers.Thedayyoustartarguingaboutsolutionstotheoretical
problemsisthedayyougoovertothedarkside.Bestrong.Stayfocused.Resistthepatternzealots.
DoYouTalkAboutGraphicDesignandLayouts?
No.Thisisn’tthatkindofbook,either.Thelayoutoftheexamplewebappsisprettysimple.Therearea
coupleofreasonsforthis.Thefirstisthatthisisabookaboutprogramming,andwhileIspendalotof
timeshowingyoutechniquesformanagingmarkupdynamically,theactualvisualeffectisverymucha
sideeffect.
ThesecondreasonisthatIhavetheartisticabilitiesofalemon.Idon’tdraw,Idon’tpaint,andI
don’thaveasidelinebusinesssellingmyoil-on-canvasworkatalocalgallery.Infact,asachildIwas
excusedfromartlessonsbecauseofatotalandabsolutelackoftalent.Iamaprettygoodprogrammer,
butmydesignskillssuck.Inthisbook,IsticktowhatIknow,whichisheavy-dutyprogramming.
4
www.it-ebooks.info
CHAPTER1GETTINGREADY
WhatIfYouDon’tLiketheTechniquesorToolsIDescribe?
Thenyouadaptthetechniquesuntilyoudolikethemandfindalternativetoolsthatworkthewayyou
prefer.Thecriticalinformationinthisbookisthatyoucanapplyheavy-dutyserver-sidetechniquesto
createbetterwebapps.Thefineimplementationdetailisn’timportant.Mypreferredtoolsand
techniquesworkwellforme,andifyouthinkaboutcodeinthewayIdo,theywillworkwellforyoutoo.
Butifyourmindworksinadifferentway,changethebitsofmyapproachthatdon’tfit,discardthebits
thatdon’twork,andusewhat’sleftasafoundationforyourownapproaches.We’llbothcomeoutahead
aslongasyouendupwithwebappsthatscalebetter,makeyourcodingmoreenjoyable,andreducethe
burdenofmaintenance.
IsThereaLotofCodeinThisBook?
Yes.Infact,thereissomuchcodethatIcouldn’tfititallin.Bookshaveapagebudget,whichissetright
atthestartoftheproject.Thepagebudgetaffectsthescheduleforthebook,theproductioncost,and
thefinalpricethatthebooksellsfor.Stickingtothepagebudgetisabigdeal,andmyeditorgets
uncomfortablewheneverhethinksIamgoingtorunlong(hi,Ben!).Ihadtodosomeeditingtofitinall
ofthecodeIwantedtoinclude.So,whenIintroduceanewtopicormakealotofchangesinonego,I’ll
showyouacompleteHTMLdocumentorJavaScriptcodefile,justliketheoneshowninListing1-1.
Listing1-1.ACompleteHTMLDocument
<!DOCTYPE html>
<html>
<head>
<title>CheeseLux</title>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script src="jquery.mobile-1.0.1.js" type="text/javascript"></script>
<link rel="stylesheet" type="text/css" href="jquery.mobile-1.0.1.css"/>
<link rel="stylesheet" type="text/css" href="styles.mobile.css"/>
<script>
function setCookie(name, value, days) {
var date = new Date();
date.setTime(date.getTime()+(days * 24 * 60 * 60 *1000));
document.cookie = name + "="+ value
+ "; expires=" + date.toGMTString() +"; path=/";
}
$(document).bind("pageinit", function() {
$('button').click(function(e) {
var useMobile = e.target.id == "yes";
var useMobileValue = useMobile ? "mobile" : "desktop";
if (localStorage) {
localStorage["cheeseLuxMode"] = useMobileValue;
} else {
setCookie("cheeseLuxMode", useMobileValue, 30);
}
location.href = useMobile ? "mobile.html" : "example.html";
});
});
</script>
5
www.it-ebooks.info
CHAPTER1GETTINGREADY
</head>
<body>
<div id="page1" data-role="page" data-theme="a">
<img class="logo" src="cheeselux.png">
<span class="para">
Would you like to use our mobile web app?
</span>
<div class="middle">
<button data-inline="true" data-theme="b" id="yes">Yes</button>
<button data-inline="true" id="no">No</button>
</div>
</div>
</body>
</html>
ThislistingisbasedononefromChapter8.Thefulllistinggivesyouawidercontextabouthowthe
techniqueathandfitsintothewebappworld.WhenIamshowingasmallchangeoremphasizinga
particularregionofcode,thenI’llshowyouacodefragmentliketheoneinListing1-2.
Listing1-2.ACodeFragment
...
<title>CheeseLux</title>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script src="jquery.mobile-1.0.1.js" type="text/javascript"></script>
<link rel="stylesheet" type="text/css" href="jquery.mobile-1.0.1.css"/>
<link rel="stylesheet" type="text/css" href="styles.mobile.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script>
...
ThesefragmentsarecumulativelyappliedtothelastfulllistingsothatthefragmentinListing1-2
showsametaelementbeingaddedtotheheadsectionofListing1-1.Youdon’thavetoapplythese
changesyourselfifyouwanttoexperimentwiththeexamples.Instead,youcandownloadacomplete
setofeverycodelistinginthisbookfromApress.com.Thisfreedownloadalsoincludestheserver-side
codethatIrefertolaterinthischapterandusethroughoutthisbooktocreatedifferentaspectsofthe
webapp.
WhatSoftwareDoYouNeedforThisBook?
Youwillneedafewpiecesofsoftwareifyouwanttore-createtheexamplesinthisbook.Therearelotsof
choicesforeachtype,andtheonesthatIuseareallavailablewithoutcharge.Idescribeeachinthe
sectionsthatfollowalongwithmypreferredtoolineachcategory.
GettingtheSourceCode
Youwillneedtodownloadthesourcecodethataccompaniesthisbook,whichisavailablewithout
chargefromApress.com.Thesourcecodedownloadcontainsallofthelistingsorganizedbychapterand
allofthesupportingresources,suchasimagesandstylesheets.Youwillneedthecontentsofthis
downloadifyouwanttocompletelyre-createanyoftheexamples.
6
www.it-ebooks.info
CHAPTER1GETTINGREADY
GettinganHTMLEditor
AlmostanyeditorcanbeusedtoworkwithHTML.Idon’trelyonanyspecialfeaturesinthisbook,so
usewhatevereditorsuitsyou.IuseKomodoEditfromActiveState.Itisfreeandsimpleandhaspretty
goodsupportforHTML,JavaScript,jQuery,andNode.js.IhavenoaffiliationwithActiveStateotherthan
asahappyuser.YoucangetKomodoEditfromhttp://activestate.com,andthereareversionsfor
Windows,Mac,andLinux.
GettingaDesktopWebBrowser
Anymodernmainstreamdesktopbrowserwillruntheexamplesinthisbook.IlikeGoogleChrome;I
finditquick,IlikethesimpleUI,andthedevelopertoolsareprettygood.Mostofthescreenshotsinthis
bookareofGoogleChrome,althoughtherearetimeswhenIuseFirefoxbecauseChromedoesn’t
implementanHTML5featurefully.(ThesupportforHTML5APIsisabitmixedasIwritethis,butevery
browserreleaseimprovesthesituation.)
GettingaMobileBrowserEmulator
InChapters7and8,Italkabouttargetingdifferentkindsofdevices.Itcanbeslowandfrustratingwork
dealingwithrealdevicesduringtheearlystagesofdevelopment,soIuseamobilebrowseremulatorto
getstartedandputthemajorfunctionalitytogether.Itisn’tuntilIhavesomethingfunctionalandsolid
thatIstarttestingonrealmobiledevices.
IliketheOperaMobileemulator,whichyoucangetforfreefrom
www.opera.com/developer/tools/mobile;thereareversionsavailableforWindows,Mac,andLinux.The
emulatorusesthesamecodebaseastherealand,widelyused,OperaMobile,andwhiletherearesome
quirks,theexperienceisprettyfaithfultotheoriginal.Ilikethispackagebecauseitletsmecreate
emulatorsfordifferentscreensizesfromsmall-screenedsmartphonesrightthroughtoHDtablets.There
issupportforemulatingtoucheventsandchangingtheorientationofthedevice.Youcanrunthe
examplesinChapters7and8inanybrowser,butpartofthepointofthesechaptersistoelegantlydetect
mobiledevices,andyou’llgetthebestresultsbyusinganemulator,evenifitisn’ttheoneforOpera.
GettingtheJavaScriptLibraries
Idon’tbelieveinre-creatingfunctionalitythatisavailableinawell-written,publicallyavailable
JavaScriptlibrary.Tothatend,thereareanumberoflibrariesthatIuseineachchapter.Some,suchas
jQuery,jQueryUI,andjQueryMobile,arewell-known,buttherearealsosomethatprovidesomeniche
featuresorcoveragapinbrowsersthatdon’timplementcertainHTML5APIs.Itellyouhowtoobtain
eachlibraryasIintroduceit,andtheycanallbefoundinthesourcecodedownloadthatisavailable
fromApress.com.Youdon’tneedtousethelibrariesthatIlikeinordertousethetechniquesIdiscuss,
butyouwillneedthemtore-createtheexamples.
GettingaWebServer
Theexamplesinthisbookarefocusedontheclient-sidewebapps,butsometechniquesrequirecertain
behaviorsfromtheserver.Mostoftheexampleswillworkwithcontentservedupbyanywebserver,but
youwillneedtouseNode.jsifyouwanttore-createeveryexampleinthisbook.
ThereasonthatIchoseNode.jsisthatitiswritteninJavaScriptandissupportedonawiderangeof
platforms.Thismeansthatanyreaderofthisbookwillbeabletosetuptheserverandreadand
understandthecodethatdrivestheserver.
7
www.it-ebooks.info
CHAPTER1GETTINGREADY
Theserver-sidecodeisincludedinthesourcecodedownloadfromApress.com,inafilecalled
server.js.Iamnotgoingtogointoanydetailaboutthiscode,andIamnotevengoingtolistit.It
doesn’tdoanythingspecial;itjustservesupcontentandhasafewspecialURLsthatallowmetopost
datafromtheexamplewebappandgetatailoredresponse.TherearesomeotherURLsthatcreate
particulareffects,suchasaddingadelaytosomerequests.Takealookatserver.jsifyouwanttosee
what’sthere,butyoudon’tneedtounderstand(orevenlookat)theserver-sidecodetogetthebestfrom
thisbook.
Youwill,however,needtoinstallandsetupNode.jssothatitisrunningonyournetwork.Iprovide
instructionsforgettingupandrunninginthesectionsthatfollow.
GettingandPreparingNode.js
YoucandownloadNode.jsfromhttp://nodejs.org.InstallationpackagesareavailableforWindows,
Mac,andLinux,andthesourcecodeisavailableifyouwanttocompileforadifferentplatform.The
instructionsforsettingupNodechangeoften,andthebestwaytogetstartedisbyreadingFelix
Geisendörfer’sbeginner’sguidetoNode,whichyoucanfindathttp://nodeguide.com/beginner.html.
Irelyonsomethird-partymodules,sorunthefollowingcommandafteryouhaveinstalledthe
Node.jspackage:
npm install node-static jqtpl
Thiscommanddownloadsandinstallsthenode-staticandjqtplpackagesthatIusetodeliver
staticandtemplatedcontentintheexamples.Thecommandwillgenerateoutputsimilartothis(but
youmayseesomeadditionalwarnings,whichcanbeignored):
npm http GET https://registry.npmjs.org/node-static
npm http GET https://registry.npmjs.org/jqtpl
npm http 200 https://registry.npmjs.org/jqtpl
npm http 200 https://registry.npmjs.org/node-static
node-static@0.5.9 ./node_modules/node-static
jqtpl@1.0.9 ./node_modules/jqtpl
Thesourcecodedownloadisorganizedbychapter.Youwillneedtocreateadirectorycalled
contentinyourNode.jsdirectoryandcopythechaptercontentintoit.Thereisn’tmuchstructuretothe
contentdirectory;tokeepthingssimple,almostalloftheresourcesandlistingsareinthesame
directory.
CautionTherearechangesintheresourcefilesbetweenchapters,somakesureyouclearyourbrowser’s
historywhenyoumovebetweenchaptercontent.
Youwillalsoneedtocopytheserver.jsfilefromthesourcecodedownloadintoyourNode.js
directory.ThisNodescriptisonlyforservingtheexamplesinthebook;don’trelyonitforanyother
purpose,andcertainlydon’tuseittohostrealprojects.Onceyouhaveeverythinginplace,simplyrun
thefollowingcommand:
8
www.it-ebooks.info
CHAPTER1GETTINGREADY
node server.js
Youwillseethefollowingoutput(orsomethingveryclosetoit):
The "sys" module is now called "util". It should have a similar interface.
Ready on port 80
IfyouareusingWindows,youmaybepromptedtoallowNodetocommunicatethroughthe
WindowsFirewall,whichyoushoulddo.Andwiththat,yourserverisupandrunning.Thescriptlistens
forrequestsonport80.Ifyouneedtochangethis,thenlookforthefollowinglineintheserver.jsfile:
http.createServer(handleRequest).listen(80);
CautionNode.jsisveryvolatile,andnewversionsarereleasedoften.TheversionthatIhaveusedinthisbook
is0.6.6,butitwillhavebeensupersededbythetimeyoureadthis.IhavestucktothemorestableNodeAPIs,but
youmightneedtomakesomeminortweakstogeteverythingworking.
IntroducingtheCheeseLuxExample
Mostoftheexamplesinthisbookarebasedonawebappforafictionalcheeseretailercalled
CheeseLux.Iwantedtofocusontheindividualtechniquesinthisbook,soIhavekeptthewebappas
simpleaspossible.Tobeginwith,Ihavecreatedastaticwebsitethatofferslimitedproductstotheuser.
Theentrypointtothesiteistheexample.htmlfile.Iuseexample.htmlforalmostallofthelistingsinthis
book.Listing1-3showstheinitialstaticversionofexample.html.
9
www.it-ebooks.info
CHAPTER1GETTINGREADY
Listing1-3.TheStaticexample.html
<!DOCTYPE html>
<html>
<head>
<title>CheeseLux</title>
<link rel="stylesheet" type="text/css" href="styles.css"/>
</head>
<body>
<div id="logobar">
<img src="cheeselux.png">
<span id="tagline">Gourmet European Cheese</span>
</div>
<form action="/basket" method="post">
<div class="cheesegroup">
<div class="grouptitle">French Cheese</div>
<div class="groupcontent">
<label for="camembert" class="cheesename">Camembert ($18)</label>
<input name="camembert" value="0"/>
</div>
<div class="groupcontent">
<label for="tomme" class="cheesename">Tomme de Savoie ($19)</label>
<input name="tomme" value="0"/>
</div>
<div class="groupcontent">
<label for="morbier" class="cheesename">Morbier ($9)</label>
<input name="morbier" value="0"/>
</div>
</div>
<div id="buttonDiv">
<input type="submit" />
</div>
</form>
</body>
</html>
Ihavestartedwithsomethingbasic.Therearefourpagesinthestaticversionofthewebapp,
althoughItendtofocusonthefunctionalityofonlythefirsttwoinlaterchapters.Thesearetheproduct
listingandabasketshowingauser’sselections(whichishandledinthestaticversionbybasket.html).
Youcanseehowexample.htmlandbasket.htmlaredisplayedinthebrowserinFigure1-1.
10
www.it-ebooks.info
CHAPTER1GETTINGREADY
Figure1-1.Theexample.htmlandbasket.htmlfilesdisplayedinthebrowser
Youdon’tneedtodoanythingwiththestaticfiles,butifyoulookatthecontentsofbasket.html,for
example,youwillseethatIusetemplatestogeneratethecontentbasedonthedatasubmittedviathe
HTMLforms,asshowninListing1-4.
Listing1-4.UsingaTemplatetoGenerateContent
<html>
<head>
<title>CheeseLux</title>
<link rel="stylesheet" type="text/css" href="styles.css"/>
</head>
<body>
<div id="logobar">
<img src="cheeselux.png">
<span id="tagline">Gourmet European Cheese</span>
</div>
<form action="/shipping" method="post">
<div class="cheesegroup">
<div class="grouptitle">Your Basket</div>
<table class="basketTable" border=0>
<thead>
<tr><th>Cheese</th><th>Quantity</th><th>Subtotal</th></tr>
11
www.it-ebooks.info
CHAPTER1GETTINGREADY
<tr><td class="sumline" colspan=3></td></tr>
</thead>
<tbody>
{{each properties}}
{{if $value.propVal > 0}}
<tr>
<td>${$data.getProp($value.propName, "name")}</td>
<td>${$value.propVal}</td>
<td>
$${$data.getSubtotal($value.propName, $value.propVal)}
</td>
</tr>
{{/if}}
{{/each}}
</tbody>
<tfoot>
<tr><td class="sumline" colspan=3></td></tr>
<tr><th colspan=2>Total:</th><td>$${$data.total}</td>
</tfoot>
</table>
<div class="cornerplaceholder"></div>
</div>
<div id="buttonDiv">
<input type="submit" />
</div>
{{each properties}}
<input type="hidden" name="${$value.propName}" value="${$value.propVal}"/>
{{/each}}
</form>
</body>
</html>
ThesetemplatesareprocessedbythejqtplmodulethatyoudownloadedforNode.js.Thismodule
isaNode-compliantversionofasimpletemplatelibrarythatiswidelyusedwiththejQuerylibrary.I
don’tusethisstyleoftemplateintheclient-sideexamples,butIwantedtoexplainthemeaningofthose
tagsincaseyouweretemptedtopeekatthestaticcontent.
Inthenextchapter,I’llusesomebasicJavaScripttechniquestocreateamoredynamicversionof
thissimpleappandthenspendtherestofthebookshowingyoumoreadvancedtechniquesyoucanuse
tocreatebetter,morescalable,andmoreresponsivewebappsforyourownprojects.
FontAttribution
Iusesomecustomwebfontsthroughoutthisbook.Thefontfilesareincludedinthesourcecode
downloadavailablefromApress.com.ThefontsIusecomefromTheLeagueofMovableType
(www.theleagueofmoveabletype.com)andfromtheGoogleWebFontsservice(www.google.com/webfonts).
12
www.it-ebooks.info
CHAPTER1GETTINGREADY
Summary
Inthischapter,Ioutlinedthecontentandstructureofthisbookandsetoutthesoftwarerequiredifyou
wanttoexperimentwiththeexamplesinthisbooks.IalsointroducedtheCheeseLuxexample,whichis
usedthroughoutthisbook.Inthenextchapter,I’llusesomebasictechniquestoenhancethestaticweb
pagesandintroducesomeofthecoretoolsthatIusethroughoutthisbook.Fromthenon,I’llshowyoua
seriesofbetter,industrial-strengthtechniquesthataretheheartofthisbook.
13
www.it-ebooks.info
CHAPTER 2
Getting Started
Inthischapter,IamgoingtoenhancetheexamplewebappIintroducedinChapter1.Thesearethe
entry-leveltechniques,andmostoftherestofthebookisdedicatedtoshowingyoudifferentwaysto
improveupontheresult.That’snottosaythattheexamplesinthischapterarenotuseful;theyare
absolutelyfineforsimplewebapps.Buttheyarenotsufficientforlargeandcomplexwebapps,whichis
whythechaptersthatfollowexplainhowyoucantakekeyconceptsfromtheworldofserver-side
developmentandapplythemtoyourwebapps.
ThischapteralsoletsmesetthefoundationforsomewebappdevelopmentprinciplesthatIwillbe
usingthroughoutthisbook.First,IwillberelyingonJavaScriptlibrarieswheneverpossiblesoasto
avoidcreatingcodethatsomeoneelsehasproducedandmaintained.ThelibraryIwillbemakingmost
useofisjQueryinordertomakeworkingwiththeDOMAPIsimplerandeasier(IexplainsomejQuery
basicsintheexamplesinthischapters).Second,IwillbefocusingonasingleHTMLdocument.
UpgradingtheSubmitButton
Togetstarted,IamgoingtouseJavaScripttoreplacethesubmitbuttonfromthebaselineexamplein
Chapter1.Thebrowsercreatesthisbuttonfromaninputelementwhosetypeissubmit,andIamgoing
toswitchitoutforsomethingthatisvisuallyconsistentwiththerestofthedocument.Morespecifically,
IamgoingtousejQuerytoreplacetheinputelement.
PreparingtoUsejQuery
TheDOMAPIiscomprehensivebutawkwardtouse—soawkwardthatthereareanumberofJavaScript
conveniencelibrariesthatwraparoundtheDOMAPIandmakeiteasiertouse.Inmyexperience,the
bestoftheselibrariesisjQuery,whichiseasytouseandactivelydevelopedandsupported.jQueryisalso
thefoundationformanyotherJavaScriptlibraries,someofwhichI’llbeusinglater.jQueryisjusta
wrapperaroundtheDOMAPI,andthisallowstheuseoftheunderlyingDOMobjectsandmethodsifit
isrequired.
YoucandownloadthejQuerylibraryfromjQuery.com.jQuery,likemostJavaScriptlibraries,is
availableintwoversions.Theuncompressedversioncontainsthefullsourcecodeandisusefulfor
developmentanddebugging.Thecompressedversion(alsoknownastheminimizedorminified
version)ismuchsmallerbutisn’thuman-readable.Thesmallersizemakestheminimizedversionideal
forsavingbandwidthwhenawebappisdeployedintoproduction.Bandwidthcanbeexpensivefor
popularwebapps,andanysavingsisworthmaking.
Downloadtheversionyouwantandputitinyourcontentdirectory,alongsideexample.html.I’llbe
usingtheuncompressedversioninthisbook,soIhavedownloadedafilecalledjquery-1.7.1.js.
15
www.it-ebooks.info
CHAPTER2GETTINGSTARTED
TipIamusingtheuncompressedversionsbecausetheymakedebuggingeasier,whichyoumayfindusefulas
youexploretheexamplesinthisbook.Forrealwebapplications,youshouldswitchtotheminimizedversionprior
todeployment.
ThefilenameincludesthejQueryversion,whichis1.7.1asIwritethis.YouimportthejQuery
libraryintotheexampledocumentusingascriptelement,asshowninListing2-1.Ihaveaddedthe
scriptelementintheheadsectionofthedocument.
Listing2-1.ImportingjQueryintotheExampleDocument
...
<head>
<title>CheeseLux</title>
<link rel="stylesheet" type="text/css" href="styles.css"/>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
</head>
...
USING A CDN FOR JQUERY
AnalternativetohostingthejQuerylibraryonyourownwebserversistouseapubliccontentdistribution
network(CDN)thathostsjQuery.ACDNisadistributednetworkofserversthatdeliverfilestotheuser
usingtheserverthatisclosesttothem.ThereareacoupleofbenefitstousingaCDN.Thefirstisafaster
experiencetotheuser,becausethejQuerylibraryfileisdownloadedfromtheserverclosesttothem,
ratherthanfromyourservers.Oftenthefilewon’tberequiredatall.jQueryissopopularthattheuser’s
browsermayhavealreadycachedthelibraryfromanotherapplicationthatalsousesjQuery.Thesecond
benefitisthatnoneofyourpreciousandexpensivebandwidthisspentdeliveringjQuerytotheuser.
WhenusingaCDN,youmusthaveconfidenceintheCDNoperator.Youwanttobesurethattheuser
receivesthefiletheyaresupposedtoandthattheservicewillalwaysbeavailable.GoogleandMicrosoft
bothprovideCDNservicesforjQuery(andotherpopularJavaScriptlibraries)freeofcharge.Both
companieshavesolidexperienceofrunninghighlyavailableservicesandareunlikelytodeliberately
tamperwiththejQuerylibrary.YoucanlearnabouttheMicrosoftserviceat
www.asp.net/ajaxlibrary/cdn.ashxandabouttheGoogleserviceat
http://code.google.com/apis/libraries/devguide.html.
TheCDNapproachisn’tsuitableforapplicationsthataredeliveredtouserswithinanintranetbecauseit
causesallthebrowserstogototheInternettogetthejQuerylibrary,ratherthanaccessthelocalserver,
whichisgenerallycloserandfasterandhaslowerbandwidthcosts.
So,let’sjumprightinandusejQuerytohidetheexistinginputelementandaddsomethingelsein
itsplace.Listing2-2showshowthisisdone.
16
www.it-ebooks.info
CHAPTER2GETTINGSTARTED
Listing2-2.HidingtheinputElementandAddingAnotherElement
<!DOCTYPE html>
<html>
<head>
<title>CheeseLux</title>
<link rel="stylesheet" type="text/css" href="styles.css"/>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script>
$(document).ready(function() {
$('#buttonDiv input:submit').hide();
$('<a href=#>Submit Order</a>').appendTo("#buttonDiv");
})
</script>
</head>
<body>
<div id="logobar">
<img src="cheeselux.png">
<span id="tagline">Gourmet European Cheese</span>
</div>
<form action="/basket" method="post">
<div class="cheesegroup">
<div class="grouptitle">French Cheese</div>
<div class="groupcontent">
<label for="camembert" class="cheesename">Camembert ($18)</label>
<input name="camembert" value="0"/>
</div>
<div class="groupcontent">
<label for="tomme" class="cheesename">Tomme de Savoie ($19)</label>
<input name="tomme" value="0"/>
</div>
<div class="groupcontent">
<label for="morbier" class="cheesename">Morbier ($9)</label>
<input name="morbier" value="0"/>
</div>
</div>
<div id="buttonDiv">
<input type="submit" />
</div>
</form>
</body>
</html>
Ihaveaddedanotherscriptelementtothedocument.Thiselementcontainsinlinecode,rather
thanloadinganexternalJavaScriptfile.Ihavedonethisbecauseitmakesiteasiertoshowyouthe
changesIammaking.UsinginlinecodeisnotajQueryrequirement,andyoucanputyourjQuerycode
17
www.it-ebooks.info
CHAPTER2GETTINGSTARTED
inexternalfilesifyouprefer.ThereisalotgoingoninthefourJavaScriptstatementsinthescript
element,soI’llbreakthingsdownstep-by-stepinthefollowingsections.
UnderstandingtheReadyEvent
AttheheartofjQueryisthe$function,whichisaconvenientshorthandtobeginusingjQueryfeatures.
ThemostcommonwaytousejQueryistotreatthe$asaJavaScriptfunctionandpassaCSSselectoror
oneormoreDOMobjectsasarguments.Usingthe$functionisverycommonwithjQuery.Ihaveusedit
threetimesinfourlinesofcode,forexample.
The$functionreturnsajQueryobjectonwhichyoucancalljQuerymethods.ThejQueryobjectisa
wrapperaroundtheelementsyouselected,andifyoupassaCSSselectorastheargument,thejQuery
objectwillcontainalloftheelementsinthedocumentthatmatchtheselectoryouspecify.
TipThisisoneofthemainadvantagesofjQueryoverthebuilt-inDOMAPI:youcanselectandmodifymultiple
elementsmoreeasily.ThemostrecentversionsoftheDOMAPI(includingtheonethatispartofHTML5)provide
supportforfindingelementsusingselectors,butjQuerydoesitmoreconciselyandelegantly.
ThefirsttimeIusethe$functioninthelisting,Ipassinthedocumentobjectastheargument.The
documentobjectistherootnodeoftheelementhierarchyintheDOM,andIhaveselecteditwiththe$
functionsothatIcancallthereadymethod,ashighlightedinListing2-3.
Listing2-3.SelectingtheDocumentandCallingthereadyMethod
...
<script>
$(document).ready(function() {
...other JavaScript statements...
})
</script>
...
BrowsersexecuteJavaScriptcodeassoonastheyfindthescriptelementsinthedocument.This
givesusaproblemwhenyouwanttomanipulatetheelementsintheDOM,becauseyourcodeis
executedbeforethebrowserhasparsedtherestoftheHTMLdocument,discoveredtheelementsthat
youwanttoworkwith,andaddedobjectstotheDOMtorepresentthem.AtbestyourJavaScriptcode
doesn’twork,andatworstyoucauseanerrorwhenthishappens.Thereareanumberofwaystowork
aroundthis.Thesimplestsolutionistoplacethescriptelementattheendofthedocumentsothatthe
browserdoesn’tdiscoverandexecuteyourJavaScriptcodeuntiltherestoftheHTMLhasbeen
processed.AmoreelegantapproachistousethejQueryreadymethod,whichishighlightedinthe
listingjustshown.
YoupassaJavaScriptfunctionastheargumenttothereadymethod,andjQuerywillexecutethis
functiononcethebrowserhasprocessedalloftheelementsinthedocument.Usingthereadymethod
allowsyoutoplaceyourscriptelementsanywhereinthedocument,safeintheknowledgethatyour
codewon’tbeexecuteduntiltherightmoment.
18
www.it-ebooks.info
CHAPTER2GETTINGSTARTED
CautionAcommonmistakeistoforgettowraptheJavaScriptstatementstobeexecutedinafunction,which
causesanoddeffect.Ifyoupassasinglestatementtothereadymethod,thenitwillbeexecutedassoonasthe
browserprocessesthescriptelement.Ifyoupassmultiplestatements,thenthebrowserwillusuallyreporta
JavaScripterror.
Thereadymethodcreatesahandlerforthereadyevent.I’llshowyoumoreofthewaythatjQuery
supportseventslaterinthischapter.Thereadyeventisavailableonlyforthedocumentobject,whichis
whyyouwillseethestatementshighlightedinthelistinginalmosteverywebappthatusesjQuery.
SelectingandHidingtheInputElement
NowthatIhavedelayedtheexecutionoftheJavaScriptcodeuntiltheDOMisready,Icanturntothe
nextstepinmytask,whichistohidetheinputelementthatsubmitstheform.Listing2-4highlightsthe
statementfromtheexamplethatdoesjustthis.
Listing2-4.SelectingandHidingtheinputElement
...
<script>
$(document).ready(function() {
$('#buttonDiv input:submit').hide();
$('<a href=#>Submit Order</a>').appendTo("#buttonDiv");
})
</script>
...
Thisisaclassictwo-partjQuerystatement:firstIselecttheelementsIwanttoworkwith,andthenI
applyajQuerymethodtomodifytheselectedelements.YoumaynotrecognizetheselectorIhaveused
becausethe:submitpartisoneoftheselectorsthatjQuerydefinesinadditiontothoseintheCSS
specification.Table2-1containsthemostusefuljQuerycustomselectors.
19
www.it-ebooks.info
CHAPTER2GETTINGSTARTED
CautionThejQuerycustomselectorscanbeextremelyuseful,buttheyhaveaperformanceimpact.Wherever
possible,jQueryusesthenativebrowsersupportforfindingelementsinthedocument,andthisisusuallypretty
quick.However,jQueryhastoprocessthecustomselectorsdifferently,sincethebrowserdoesn’tknowanything
aboutthem,andthistakeslongerthanthenativeapproach.Thisperformancedifferencedoesn’tmatterformost
webapps,butifperformanceiscritical,youmaywanttostickwiththestandardCSSselectors.
Table2-1.jQueryCustomSelectors
Selector
Description
:button
Selectsallbuttons
:checkbox
Selectsallcheckboxes
:contains(text)
Selectselementsthatcontainthespecifiedtext
:eq(n)
Selectstheelementatthenthindex(zero-based)
:even
Selectsalltheevent-numberedelements(one-based)
:first
Selectsthefirstmatchedelement
:has(selector)
Selectselementsthatcontainatleastoneelementthatmatchestheselector
:hidden
Selectsallhiddenelements
:input
Selectsallinputelements
:last
Selectsthelastmatchedelement
:odd
Selectsalltheodd-numberedelements(one-based)
:password
Selectsallpasswordelements
:radio
Selectsallradioelement
:submit
Selectsallformsubmissionelements
:visible
Selectsallvisibleelements
20
www.it-ebooks.info
CHAPTER2GETTINGSTARTED
InListing2-4,myselectormatchesanyinputelementwhosetypeissubmitandthatisadescendant
oftheelementwhoseidattributeisbuttonDiv.Ididn’tneedtobequitesoprecisewiththeselector,
giventhatitistheonlysubmitelementinthedocument,butIwantedtodemonstratethejQuery
supportforselectors.The$functionreturnsajQueryobjectthatcontainstheselectedelements,
althoughthereisonlyoneelementthatmatchestheselectorinthiscase.
Havingselectedtheelement,Ithencallthehidemethod,whichchangesthevisibilityoftheselected
elementsbysettingtheCSSdisplaypropertytonone.Theinputelementislikethisbeforethemethod
call:
<input type="submit">
andistransformedlikethisafterthemethodcall:
<input type="submit" style="display: none; ">
Thebrowserwon’tshowelementswhosedisplaypropertyisnoneandsotheinputelement
becomesinvisible.
TipThecounterparttothehidemethodisshow,whichremovesthedisplaysettingandreturnstheelement
toitsvisiblestate.Idemonstratetheshowmethodlaterinthischapter.
InsertingtheNewElement
Next,Iwanttoinsertanewelementintothedocument.Listing2-5highlightsthestatementinthe
examplethatdoesthis.
Listing2-5.AddingaNewelementtotheDocument
...
<script>
$(document).ready(function() {
$('#buttonDiv input:submit').hide();
$('<a href=#>Submit Order</a>').appendTo("#buttonDiv");
})
</script>
...
Inthisstatement,IhavepassedanHTMLfragmentstringtothejQuery$function.Thiscauses
jQuerytoparsethefragmentandcreateasetofobjectstorepresenttheelementsitcontains.These
elementobjectsarethenreturnedtomeinajQueryobject,justasifIhadselectedelementsfromthe
documentitself,exceptthatthebrowserdoesn’tyetknowabouttheseelementsandtheyarenotyetpart
oftheDOM.
ThereisonlyoneelementintheHTMLfragmentinthislisting,sothejQueryobjectcontainsana
element.ToaddthiselementtotheDOM,IcalltheappendTomethodonthejQueryobject,passingina
CSSselector,whichtellsjQuerywhereinthedocumentIwanttheelementtobeinserted.
TheappendTomethodinsertsmynewelementasthelastchildoftheelementsmatchedbythe
selector.Inthiscase,IspecifiedthebuttonDivelement,whichmeansthattheelementsinmyHTML
fragmentareinsertedalongsidethehiddeninputelement,likethis:
21
www.it-ebooks.info
CHAPTER2GETTINGSTARTED
...
<div id="buttonDiv">
<input type="submit" style="display: none; ">
<a href="#">Submit Order</a>
</div>
...
TipIftheselectorthatIpassedtotheappendTomethodhadmatchedmultipleelements,thenjQuerywould
duplicatetheelementsfromtheHTMLfragmentandinsertacopyasthelastchildofeverymatchedelement.
jQuerydefinesanumberofmethodsthatyoucanusetoinsertchildelementsintothedocument,
andthemostusefulofthesearedescribedinTable2-2.Whenyouappendelements,theybecomethe
lastchildrenoftheirparentelement.Whenyouprependelements,theybecomethefirstchildrenoftheir
parents.(I’llexplainwhytherearetwoappendandtwoprependmethodslaterinthischapter.)
Table2-2.jQueryMethodsforInsertingElementsintheDocument
Method
Description
append(HTML)
append(jQuery)
Insertsthespecifiedelementsasthelastchildrenofalltheelements
intheDOM
prepend(HTML)
prepend(jQuery)
Insertsthespecifiedelementsasthefirstchildrenofalltheelements
intheDOM
appendTo(HTML)
appendTo(jQuery)
InsertstheelementsinthejQueryobjectasthelastchildrenofthe
elementsspecifiedbytheargument
prependTo(HTML)
prependTo(jQuery)
InsertstheelementsinthejQueryobjectasthefirstchildrenofthe
elementsspecifiedbytheargument
ApplyingaCSSClass
Inthepreviousexample,Iinsertedanaelement,butIdidnotassignittoaCSSclass.Listing2-6shows
howIcancorrectthisomissionbymakingacalltotheaddClassmethod.
22
www.it-ebooks.info
CHAPTER2GETTINGSTARTED
Listing2-6.ChainingjQueryMethodCalls
...
<script>
$(document).ready(function() {
$('#buttonDiv input:submit').hide();
$('<a href=#>Submit Order</a>').appendTo("#buttonDiv").addClass("button");
})
</script>
...
NoticehowIhavesimplyaddedthecalltotheaddClassmethodtotheendofthestatement.Thisis
knownasmethodchaining,andalibrarythatsupportsmethodchainingissaidtohaveafluentAPI.
MostjQuerymethodsreturnthesamejQueryobjectonwhichthemethodwascalled.Inthe
example,IcreatethejQueryobjectbypassinganHTMLfragmenttothe$function.Thisproducesa
jQueryobjectthatcontainsanaelement.TheappendTomethodinsertstheelementintothedocument
andreturnsajQueryobjectthatcontainsthesameaelementasitsresult.Thisallowsmetomakefurther
methodcalls,suchastheonetoaddClass.FluentAPIscantakeawhiletogetusedto,buttheyenable
conciseandexpressivecodeandreduceduplication.
TheaddClassmethodaddstheclassspecifiedbytheargumenttotheselectedelements,likethis:
...
<div id="buttonDiv">
<input type="submit" style="display: none; ">
<a href="#" class="button">Submit Order</a>
</div>
...
Thea.buttonclassisdefinedinstyles.cssandbringstheappearanceoftheaelementintoline
withtherestofthedocument.
UNDERSTANDING METHOD PAIRS AND METHOD CHAINING
IfyoulookatthemethodsdescribedinTable2-2,youwillseethatyoucanappendorprependelementsin
twoways.TheelementsyouareinsertingeithercanbecontainedinthejQueryobjectonwhichyoucalla
methodorcanbeinthemethodargument.jQueryprovidesdifferentmethodssoyoucanselectwhich
elementsarecontainedinthejQueryobjectformethodchaining.Inmyexample,IusedtheappendTo
method,whichmeansIcanarrangethingssothatthejQueryobjectcontainstheelementparsedfromthe
HTMLfragment,allowingmetochainthecalltotheaddClassmethodandhavetheclassappliedtothea
element.
Theappendmethodreversestherelationshipbetweentheparentandchildelements,likethis:
$('#buttonDiv').append('<a href=#>Submit Order</a>').addClass("button");
Inthisstatement,IselecttheparentelementandprovidetheHTMLfragmentasthemethodargument.
TheappendmethodreturnsajQueryobjectthatcontainsthebuttonDivelement,sotheaddClasstakes
effectontheparentdivelementratherthanthenewaelement.
23
www.it-ebooks.info
CHAPTER2GETTINGSTARTED
Torecap,Ihavehiddentheoriginalinputelement,addedanaelement,and,finally,assignedthea
elementtothebuttonclass.YoucanseetheresultinFigure2-1.
Figure2-1.Replacingthestandardformsubmitbutton
Withfourlinesofcode(onlytwoofwhichmanipulatetheDOM),Ihaveupgradedthestandard
submitbuttontosomethingconsistentwiththerestofthewebapp.AsIsaidatthestartofthischapter,
alittlecodecanleadtosignificantenhancements.
RespondingtoEvents
Iamnotquitedonewiththenewaelement.Thebrowserknowsthataninputelementwhosetype
attributeissubmitshouldsubmittheHTMLformtotheserver,anditperformsthisactionautomatically
whenthebuttonisclicked.
TheaelementthatIaddedtotheDOMlookslikeabutton,butthebrowserdoesn’tknowwhatthe
elementisforandsodoesn’tapplythesameautomaticaction.IhavetoaddsomeJavaScriptcodethat
willcompletetheeffectandmaketheaelementbehavelikeabuttonandnotjustlooklikeone.
Youdothisbyrespondingtoevents.Aneventisamessagethatissentbythebrowserwhenthestate
ofanelementchanges,forexample,whentheuserclickstheelementormovesthemouseoverit.You
tellthebrowserwhicheventsyouareinterestinginandprovideJavaScriptcallbackfunctionsthatare
executedwheneventoccurs.Aneventissaidtohavebeentriggeredwhenitissentbythebrowser,and
thecallbackfunctionsareresponsibleforhandlingtheevent.Inthefollowingsections,I’llshowyou
howtohandleeventstocompletethefunctionalityofthesubstitutebutton.
24
www.it-ebooks.info
4
CHAPTER2GETTINGSTARTED
HandlingtheClickEvent
Themostimportantforthisexampleisclick,whichistriggeredwhentheuserpressesandreleasesthe
mousebutton(inotherwords,whentheuserclicks)anelement.Forthisexample,Iwanttohandlethe
clickeventbysubmittingtheHTMLformtotheserver.TheDOMAPIprovidessupportfordealingwith
events,butjQueryprovidesamoreelegantalternative,whichyoucanseeinListing2-7.
Listing2-7.HandlingtheclickEvent
<!DOCTYPE html>
<html>
<head>
<title>CheeseLux</title>
<link rel="stylesheet" type="text/css" href="styles.css"/>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script>
$(document).ready(function() {
$('#buttonDiv input:submit').hide();
$('<a href=#>Submit Order</a>').appendTo("#buttonDiv")
.addClass("button").click(function() {
$('form').submit();
})
})
</script>
</head>
<body>
<div id="logobar">
<img src="cheeselux.png">
<span id="tagline">Gourmet European Cheese</span>
</div>
<form action="/basket" method="post">
<div class="cheesegroup">
<div class="grouptitle">French Cheese</div>
<div class="groupcontent">
<label for="camembert" class="cheesename">Camembert ($18)</label>
<input name="camembert" value="0"/>
</div>
<div class="groupcontent">
<label for="tomme" class="cheesename">Tomme de Savoie ($19)</label>
<input name="tomme" value="0"/>
</div>
<div class="groupcontent">
<label for="morbier" class="cheesename">Morbier ($9)</label>
<input name="morbier" value="0"/>
</div>
</div>
25
www.it-ebooks.info
CHAPTER2GETTINGSTARTED
<div id="buttonDiv">
<input type="submit" />
</div>
</form>
</body>
</html>
jQueryprovidessomehelpfulmethodsthatmakehandlingcommoneventssimple.Theseevents
arenamedaftertheevent;so,theclickmethodregistersthecallbackfunctionpassedasthemethod
argumentasahandlerfortheclickevent.Ihavechainedthecalltotheclickeventtotheother
methodsthatcreateandformattheaelement.Tosubmittheform,Iselecttheformelementbytypeand
callthesubmitmethod.That’sallthereistoit.Inowhavethebasicfunctionalityofthebuttoninplace.
Notonlydoesithavethesamevisualstyleastherestofthewebapp,butclickingthebuttonwillsubmit
theformtotheserver,justastheoriginalbuttondid.
HandlingMouseHoverEvents
TherearetwoothereventsthatIwanttohandletocompletethebuttonfunctionality;theyare
mouseenterandmouseleave.Themouseentereventistriggeredwhenthemousepointerismovedoverthe
element,andthemouseleaveeventistriggeredthemouseleavestheelement.
Iwanttohandletheseeventstogivetheuseravisualcuethatthebuttoncanbeclicked,andIdo
thisbychangingthestyleofthebuttonwhenthemouseisovertheelement.Theeasiestwaytohandle
theseeventsistousethejQueryhovermethod,asshowninListing2-8.
Listing2-8.UsingthejQueryhoverMethod
...
<script>
$(document).ready(function() {
$('#buttonDiv input:submit').hide();
$('<a href=#>Submit Order</a>').appendTo("#buttonDiv")
.addClass("button").click(function() {
$('form').submit();
})
.hover(
function(){
$('#buttonDiv a').addClass("buttonHover");
}, function() {
$('#buttonDiv a').removeClass("buttonHover");
})
})
</script>
...
Thehovermethodtakestwofunctionsasarguments.Thefirstfunctionisexecutedwhenthe
mouseentereventistriggered,andthesecondfunctionistriggeredinresponsetothemouseleaveevent.
Inthisexample,IhaveusedthesefunctionstoaddandremovethebuttonHoverclassfromthea
element.ThisclasschangesthevalueoftheCSSbackground-colorpropertytohighlightthebuttonwhen
themouseispositionedabovetheelement.YoucanseetheeffectinFigure2-2.
26
www.it-ebooks.info
CHAPTER2GETTINGSTARTED
Figure2-2.Usingeventstoapplyaclasstoanelement
UsingtheEventObject
ThetwofunctionsthatIpassedasargumentstothehovermethodinthepreviousexamplearelargely
thesame.Icancollapsethesetwofunctionsintoasinglehandlerthatcanprocessbothevents,asshown
inListing2-9.
Listing2-9.HandlingMultipleEventsinaSingleHandlerFunction
...
<script>
$(document).ready(function() {
$('#buttonDiv input:submit').hide();
$('<a href=#>Submit Order</a>').appendTo("#buttonDiv")
.addClass("button").click(function() {
$('form').submit();
}).hover(function(e){
var elem = $('#buttonDiv a')
if (e.type == "mouseenter") {
elem.addClass("buttonHover");
} else {
elem.removeClass("buttonHover");
}
})
})
</script>
...
Thecallbackfunctioninthisexampletakesanargument,e.ThisargumentisanEventobject
providedbythebrowsertogiveyouinformationabouttheeventyouarehandling.Ihaveusedthe
Event.typepropertytodifferentiatebetweenthetypesofeventsthatmyfunctionexpects.Thetype
propertyreturnsastringthatcontainstheeventname.Iftheeventnameismouseenter,thenIcallthe
addClassmethod.Ifnot,IcalltheremoveClassmethodthathastheeffectofremovingthespecifiedclass
fromtheclassattributeoftheelementsinthejQueryobject,theoppositeeffectoftheaddClassmethod.
27
www.it-ebooks.info
CHAPTER2GETTINGSTARTED
DealingwithDefaultActions
Tomakelifeeasierfortheprogrammer,thebrowserperformssomeactionsautomaticallywhencertain
eventsaretriggeredforspecificelementtypes.Theseareknownasdefaultactions,andtheymeanyou
don’thavetocreateeventhandlersforeverysingleeventandelementinanHTMLdocument.For
example,thebrowserwillnavigatetotheURLspecifiedbythehrefattributeofanaelementinresponse
totheclickevent.Thisisthebasisfornavigationinawebpage.
Icheatedalittlebysettingthehrefattributeto#.Thisisacommontechniquewhendefining
elementswhoseactionsaregoingtobemanagedbyJavaScriptbecausethebrowserwon’tnavigateaway
fromthecurrentdocumentwhenthedefaultactionisperformed.Inotherwords,Idon’thavetoworry
aboutthedefaultactionbecauseitdoesn’treallydoanythingthattheuserwillnotice.
Defaultactionscanbemoreimportantwhenyouneedtochangethebehavioroftheelementand
youcan’tdolittletrickslikeusing#asaURL.Listing2-10providesademonstration,whereIhave
changedthehrefattributefortheaelementtoarealwebpage.Ihaveusedtheattrmethodtosetthe
hrefattributeoftheaelementtohttp://apress.com.Withthismodification,clickingtheelement
doesn’tsubmittheformanymore;itnavigatestotheApresswebsite.
Listing2-10.ManagingDefaultActions
...
<script>
$(document).ready(function() {
$('#buttonDiv input:submit').hide();
$('<a href=#>Submit Order</a>')
.appendTo("#buttonDiv")
.attr("href", "http://apress.com")
.addClass("button").click(function() {
$('form').submit();
}).hover(function(e){
var elem = $('#buttonDiv a')
if (e.type == "mouseenter") {
elem.addClass("buttonHover");
} else {
elem.removeClass("buttonHover");
}
})
})
</script>
...
Tofixthis,acalltothepreventDefaultmethodontheEventobjectpassedtotheeventhandler
functionisrequired.Thisdisablesthedefaultactionfortheevent,meaningthatonlythecodeinthe
eventhandlerfunctionwillbeused.YoucanseetheuseofthismethodinListing2-11.
28
www.it-ebooks.info
CHAPTER2GETTINGSTARTED
Listing2-11.PreventingtheDefaultAction
...
<script>
$(document).ready(function() {
$('#buttonDiv input:submit').hide();
$('<a href=#>Submit Order</a>')
.appendTo("#buttonDiv")
.attr("href", "http://apress.com")
.addClass("button").click(function(e) {
$('form').submit();
e.preventDefault();
}).hover(function(e){
var elem = $('#buttonDiv a')
if (e.type == "mouseenter") {
elem.addClass("buttonHover");
} else {
elem.removeClass("buttonHover");
}
})
})
</script>
...
Thereisnodefaultactionforthemouseenterandmouseleaveeventsonanaelement,sointhis
listing,IneedonlytocallthepreventDefaultmethodwhenhandlingtheclickevent.WhenIclickthe
elementnow,theformissubmitted,andthehrefattributevaluedoesn’thaveanyeffect.
AddingDynamicBasketData
Youhaveseenhowyoucanimproveawebapplicationsimplybyaddingandmodifyingelementsand
handlingevents.Inthissection,Igoonestepfurthertodemonstratehowyoucanusethesesimple
techniquestocreateamoreresponsiveversionofthecheeseshopbyincorporatingtheinformation
displayedinthebasketphasealongsidetheproductselection.Ihavecalledthisadynamicbasket
becauseIwillbeupdatingtheinformationshowntouserswhentheychangethequantitiesofindividual
cheeseproducts,ratherthanthestaticbasket,whichisshownwhenuserssubmittheirselectionsusing
theunenhancedversionofthiswebapp.
AddingtheBasketElements
ThefirststepistoaddtheadditionalelementsIneedtothedocument.Icouldaddtheelementsusing
HTMLfragmentsandtheappendTomethod,butforvarietyIamgoingtouseanothertechnique,known
aslatentcontent.LatentcontentreferstoHTMLelementsthatareinthedocumentbutarehiddenusing
CSSandarerevealedandmanagedusingJavaScript.Thoseuserswhodon’thaveJavaScriptenabled
won’tseetheelementsandwillgetthebasicfunctionality,butonceIrevealtheelementsandsetupmy
eventhandling,thoseuserswithJavaScriptwillgetaricherandmorepolishedexperience.Listing2-12
showstheadditionofthelatentcontenttotheHTMLdocument.
29
www.it-ebooks.info
CHAPTER2GETTINGSTARTED
Listing2-12.AddingHiddenElementstotheHTMLDocument
<!DOCTYPE html>
<html>
<head>
<title>CheeseLux</title>
<link rel="stylesheet" type="text/css" href="styles.css"/>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script>
$(document).ready(function() {
$('#buttonDiv input:submit').hide();
$('<a href=#>Submit Order</a>')
.appendTo("#buttonDiv").addClass("button").click(function(e) {
$('form').submit();
e.preventDefault();
}).hover(function(e){
var elem = $('#buttonDiv a')
if (e.type == "mouseenter") {
elem.addClass("buttonHover");
} else {
elem.removeClass("buttonHover");
}
})
})
</script>
</head>
<body>
<div id="logobar">
<img src="cheeselux.png">
<span id="tagline">Gourmet European Cheese</span>
</div>
<form action="/basket" method="post">
<div class="cheesegroup">
<div class="grouptitle">French Cheese</div>
<div class="groupcontent">
<label for="camembert" class="cheesename">Camembert ($18)</label>
<input name="camembert" value="0"/>
<span class="subtotal latent">($<span>0</span>)</span>
</div>
<div class="groupcontent">
<label for="tomme" class="cheesename">Tomme de Savoie ($19)</label>
<input name="tomme" value="0"/>
<span class="subtotal latent">($<span>0</span>)</span>
</div>
<div class="groupcontent">
<label for="morbier" class="cheesename">Morbier ($9)</label>
30
www.it-ebooks.info
CHAPTER2GETTINGSTARTED
<input name="morbier" value="0"/>
<span class="subtotal latent">($<span>0</span>)</span>
</div>
<div class="sumline latent"></div>
<div class="groupcontent latent">
<label class="cheesename">Total:</label>
<input class="placeholder" name="spacer" value="0"/>
<span class="subtotal latent" id="total">$0</span>
</div>
</div>
<div id="buttonDiv">
<input type="submit" />
</div>
</form>
</body>
</html>
Ihavehighlightedtheadditionalelementsinthelisting.Theyareallassignedtothelatentclass,
whichhasthefollowingdefinitioninthestyles.cssfile:
...
.latent {
display: none;
}
...
IshowedyouearlierinthechapterthatthejQueryhidemethodsetstheCSSdisplaypropertyto
nonetohideelementsfromtheuser,andIhavefollowedthesameapproachwhensettingupthisclass.
Theelementsareinthedocumentbutnotvisibletotheuser.
ShowingtheLatentContent
Nowthatthelatentelementsareinplace,IcanworkwiththemusingjQuery.Thefirststepistoreveal
themtotheuser.SinceIammanipulatingtheelementsusingJavaScript,theywillberevealedonlyto
userswhohaveJavaScriptenabled.Listing2-13showstheadditiontothescriptelement.
Listing2-13.RevealingtheLatentContent
...
<script>
$(document).ready(function() {
$('#buttonDiv input:submit').hide();
$('<a href=#>Submit Order</a>')
.appendTo("#buttonDiv").addClass("button").click(function(e) {
$('form').submit();
e.preventDefault();
}).hover(function(e){
var elem = $('#buttonDiv a')
if (e.type == "mouseenter") {
elem.addClass("buttonHover");
31
www.it-ebooks.info
CHAPTER2GETTINGSTARTED
});
} else {
elem.removeClass("buttonHover");
}
$('.latent').show();
})
</script>
...
Thehighlightedstatementselectsalloftheelementsthataremembersofthelatentclassandthen
callstheshowmethod.Theshowmethodaddsastyleattributetoeachselectedelementthatsetsthe
displaypropertytoinline,whichhastheeffectofrevealingtheelements.Theelementsarestill
membersofthelatentclass,butvaluesdefinedinastyleattributeoverridethosethataredefinedina
styleelement,andsotheelementsbecomevisible.
RespondingtoUserInput
Tocreateadynamicbasket,Iwanttobeabletodisplaysubtotalsforeachitemandanoveralltotal
whenevertheuserchangesaquantityforaproduct.IamgoingtohandletwoeventstogettheeffectI
want.Thefirsteventischange,whichistriggeredwhentheuserentersanewvalueandthenmovesthe
focustoanotherelement.Thesecondeventiskeyup,whichistriggeredwhentheuserreleasesakey,
havingpreviouslypressedit.ThecombinationofthesetwoeventsmeansIcanbeconfidentthatIwillbe
abletorespondsmoothlytonewvalues.jQuerydefineschangeandkeyupmethodsthatIcoulduseinthe
samewayIusedtheclickmethodearlier,butsinceIwanttohandlebotheventsinthesameway,Iam
goingtousethebindmethodinstead,asshowninListing2-14.
Listing2-14.BindingtothechangeandkeyupEvents
...
<script>
var priceData = {
camembert: 18,
tomme: 19,
morbier: 9
}
$(document).ready(function() {
$('#buttonDiv input:submit').hide();
$('<a href=#>Submit Order</a>')
.appendTo("#buttonDiv").addClass("button").click(function(e) {
$('form').submit();
e.preventDefault();
}).hover(function(e){
var elem = $('#buttonDiv a')
if (e.type == "mouseenter") {
elem.addClass("buttonHover");
} else {
elem.removeClass("buttonHover");
32
www.it-ebooks.info
CHAPTER2GETTINGSTARTED
})
}
$('.latent').show();
$('input').bind("change keyup", function() {
var subtotal = $(this).val() * priceData[this.name];
$(this).siblings("span").children("span").text(subtotal)
})
})
</script>
...
Theadvantageofthebindmethodisthatitletsmehandlemultipleeventsusingthesame
anonymousJavaScriptfunction.Todothis,Ihaveselectedtheinputelementsinthedocumenttogeta
jQueryobjectandcalledthebindmethodonit.Thefirstargumenttothebindmethodisastring
containingthenamesoftheeventstohandle,whereeventnamesareseparatedbythespacecharacter.
Thesecondargumentisthefunctionthatwillhandletheeventswhentheyaretriggered.Thereareonly
twostatementsintheeventhandlerfunction,buttheyareworthunpackingbecausetheycontainan
interestingmixofjQuery,theDOMAPI,andpureJavaScript.
TipHandlingtwoeventslikethismeansthatmycallbackfunctionmayendupbeinginvokedwhenitdoesn’t
reallyneedtobe.Forexample,iftheuserpressestheTabkey,thefocuswillchangetothenextelement,andboth
thechangeandkeyupeventswillbetriggered,eventhoughthevalueintheinputelementhasn’tchanged.Itend
towardacceptingthisduplicationasthecostofensuringafluiduserexperience.I’drathermyfunctionwas
executedmoreoftenthanreallyneededandnotmissanyuserinteraction.
CalculatingtheSubtotal
Thefirststatementinthefunctionisresponsibleforcalculatingthesubtotalforthecheeseproduct
whoseinputvaluehaschanged.Hereisthestatement:
var subtotal = $(this).val() * priceData[this.name];
WhenhandlinganeventwithjQuery,youcanusethevariablecalledthistorefertotheelement
thattriggeredtheevent.ThethisvariableisanHTMLElementobject,whichiswhattheDOMAPIusesto
representelementsinthedocument.ThereareacoresetofpropertiesdefinedbytheHTMLElement,the
mostimportantofwhicharedescribedinTable2-3.
33
www.it-ebooks.info
CHAPTER2GETTINGSTARTED
Table2-3.BasicHTMLElementProperties
Property
Description
className
Getsorsetsthelistofclassesthattheelementbelongsto
id
Getsorsetsthevalueoftheidattribute
tagName
Returnsthetagname(indicatingtheelementtype)
Thecorepropertiesaresupplementedtoaccommodatetheuniquecharacteristicsofdifferent
elementtypes.Anexampleofthisisthenameproperty,whichreturnsthevalueofthenameattributeon
thoseelementsthatsupportit,includingtheinputelement.Ihaveusedthispropertyonthethis
variabletogetthenameoftheinputelementsothatIcan,inturn,useittogetavaluefromthe
priceDataobjectthatIaddedtothescript:
var subtotal = $(this).val() * priceData[this.name];
ThepriceDataobjectisasimpleJavaScriptobjectthathasonepropertycorrespondingtoeachkind
ofcheeseandwherethevalueofeachpropertyisthepriceforthecheese.
ThethisvariablecanalsobeusedtocreatejQueryobjects,likethis:
var subtotal = $(this).val() * priceData[this.name];
BypassinganHTMLElementobjectastheargumenttothejQuery$function,IhavecreatedajQuery
objectthatactsjustasthoughIhadselectedtheelementusingaCSSselector.Thisallowsmetoeasily
applyjQuerymethodstoobjectsfromtheDOMAPI.Inthisstatement,Icallthevalmethod,which
returnsthevalueofthevalueattributeofthefirstelementinthejQueryobject.
TipThereisonlyoneelementinmyjQueryobject,butjQuerymethodsaredesignedtoworkwithmultiple
elements.Whenyouuseamethodlikevaltoreadsomevaluefromtheelement,yougetthevaluefromthefirst
elementintheselection,butwhenyouusethesamemethodtosetthevalue(bypassingthevalueasan
argument),alloftheselectedelementsaremodified.
Usingthethisvariable,Ihavebeenabletogetthevalueoftheinputelementthattriggeredthe
eventandthepricefortheproductassociatedwithit.Ithenmultiplythepriceandthequantitytogether
todeterminethesubtotal,whichIassigntoalocalvariablecalled,simplyenough,subtotal.
DisplayingtheSubtotal
Thesecondstatementinthehandlerfunctionisresponsiblefordisplayingthesubtotaltotheuser.This
statementalsooperatesintwoparts.Thefirstpartselectstheelementthatwillbeusedtodisplaythe
value:
$(this).siblings("span").children("span").text(subtotal)
34
www.it-ebooks.info
CHAPTER2GETTINGSTARTED
Onceagain,IcreateajQueryobjectusingthethisvariable.Imakeacalltothesiblingsmethod,
whichreturnsajQueryobjectthatcontainsanysiblingtotheelementsintheoriginaljQueryobjectthat
matchesthespecifiedCSSselector.ThismethodreturnsajQueryobjectthatcontainsthelatentspan
elementnexttotheinputelementthattriggeredtheevent.
Ichainacalltothechildrenmethod,whichreturnsajQueryobjectthatcontainsanychildrenofthe
elementinthepreviousjQueryobjectthatmatchthespecifiedselector.IendupwithajQueryobject
thatcontainsthenestedspanelement.Icouldhavesimplifiedtheselectorsinthisexample,butIwanted
todemonstratehowjQuerysupportsnavigationthroughtheelementsinadocumentandhowthe
contentsofthejQueryobjectinachainofmethodcallschanges.Thesechangesaredescribedin
Table2-4.
Table2-4.BasicHTMLElementProperties
Method Call
Contents of jQuery Object
$(this)
Theinputelementthattriggeredtheevent
.siblings("span")
Thespanelementthatisasiblingtotheinputelementthattriggeredthe
event
.children("span")
Thespanelementthatisachildofthespanelementthatisasiblingtothe
inputelementthattriggeredtheevent
Bycombiningmethodcallslikethis,Iamabletonavigatethroughtheelementhierarchytocreatea
jQueryobjectthatcontainspreciselytheelementorelementsIwanttoworkwith,inthiscase,thechild
ofasiblingtowhicheverelementtriggeredanevent.
Thesecondpartofthestatementisacalltothetextmethod,whichsetsthetextcontentofthe
elementsinajQueryobject.Inthiscase,thetextisthevalueofthesubtotalvariable:
$(this).siblings("span").children("span").text(subtotal)
Thenetresultisthatthesubtotalforacheeseisupdatedassoonasauserchangesthequantity
required.
CalculatingtheOverallTotal
Tocompletethebasket,Ineedtogenerateanoveralltotaleachtimeasubtotalchanges.Ihavedefineda
newfunctioninthescriptelementandaddedacalltoitintheeventhandlerfunctionfortheinput
elements.Listing2-15showstheadditions.
35
www.it-ebooks.info
CHAPTER2GETTINGSTARTED
Listing2-15.CalculatingtheOverallTotal
...
<script>
var priceData = {
camembert: 18,
tomme: 19,
morbier: 9
}
$(document).ready(function() {
$('#buttonDiv input:submit').hide();
$('<a href=#>Submit Order</a>')
.appendTo("#buttonDiv").addClass("button").click(function(e) {
$('form').submit();
e.preventDefault();
}).hover(function(e){
var elem = $('#buttonDiv a')
if (e.type == "mouseenter") {
elem.addClass("buttonHover");
} else {
elem.removeClass("buttonHover");
}
})
$('.latent').show();
$('input').bind("change keyup", function() {
var subtotal = $(this).val() * priceData[this.name];
$(this).siblings("span").children("span").text(subtotal)
calculateTotal();
})
})
function calculateTotal() {
var total = 0;
$('span.subtotal span').not('#total').each(function(index, elem) {
total += Number($(elem).text());
})
$('#total').text("$" + total);
}
</script>
...
ThefirststatementinthecalculateTotalfunctiondefinesalocalvariableandinitializestozero.I
usethisvariabletosumtheindividualsubtotals.Thenextstatementisthemostinterestingoneinthis
function.Thefirstpartofthestatementselectsasetofelements:
36
www.it-ebooks.info
CHAPTER2GETTINGSTARTED
...
$('span.subtotal span').not('#total').each(function(index, elem) {
...
Istartbyselectingallspanelementsthataredescendantsofspanelementsthatarepartofthe
subtotalclass.Thisisanotherwayofselectingthesubtotalelements.Ithenusethenotmethodto
removeelementsfromtheselection.Inthiscase,Iremovetheelementwhoseidistotal.Idothis
becauseIdefinedthesubtotalandtotalelementsusingthesameclassesandstyles,andIdon’twantthe
currenttotaltobeincludedwhencalculatinganewtotal.
Havingselectedtheitems,Ithenusetheeachmethod.Thismethodcallsafunctiononceforeach
elementinajQueryobject.Theargumentstothefunctionaretheindexofthecurrentelementinthe
selectionandtheHTMLElementobjectthatrepresentstheelementintheDOM.
Igetthecontentofeachsubtotalelementusingthetextmethod.IcreateajQueryobjectbypassing
theHTMLElementobjectasanargumenttothe$function,justasIdidwiththethisvariableearlierinthis
chapter.
Thetextmethodreturnsastring,soIusetheJavaScriptNumberfunctiontocreateanumericvalue
thatIcanaddtotherunningtotal:
total += Number($(elem).text());
Finally,Iselectthetotalelementandusethetextmethodtodisplaytheoveralltotal:
$('#total').text("$" + total);
Theeffectofaddingthisfunctionisthatachangeinthequantityforacheeseisimmediately
reflectedinthetotal,aswellasintheindividualsubtotals.
ChangingtheFormTarget
Byaddingadynamicbasket,Ihavepulledthefunctionalityofthebasketwebpageintothemainpageof
theapplication.Itdoesn’tmakesensetosendJavaScript-enableduserstothebasketwebpagewhen
theysubmittheform,becauseitjustduplicatedinformationtheyhavealreadyseen.Iamgoingto
changethetargetoftheformelementsothatsubmittingtheformgoesstraighttotheshippingpage,
skippingoverthebasketpageentirely.Listing2-16showsthestatementthatchangesthetarget.
Listing2-16.ChangingtheTargetfortheformElement
...
<script>
var priceData = {
camembert: 18,
tomme: 19,
morbier: 9
}
$(document).ready(function() {
$('#buttonDiv input:submit').hide();
$('<a href=#>Submit Order</a>')
.appendTo("#buttonDiv").addClass("button").click(function(e) {
$('form').submit();
e.preventDefault();
37
www.it-ebooks.info
CHAPTER2GETTINGSTARTED
}).hover(function(e){
var elem = $('#buttonDiv a')
if (e.type == "mouseenter") {
elem.addClass("buttonHover");
} else {
elem.removeClass("buttonHover");
}
})
$('.latent').show();
$('input').bind("change keyup", function() {
var subtotal = $(this).val() * priceData[this.name];
$(this).siblings("span").children("span").text(subtotal)
calculateTotal();
})
$('form').attr("action", "/shipping");
})
function calculateTotal() {
var total = 0;
$('span.subtotal span').not('#total').each(function(index, elem) {
total += Number($(elem).text());
})
$('#total').text("$" + total);
}
</script>
...
Bythispoint,itshouldbeobvioushowthenewstatementworks.Iselecttheformelementbytype
(sincethereisonlyonesuchelementinthedocument)andcalltheattrmethodtosetanewvaluefor
theactionattribute.Theuseristakentotheshippingdetailspagewhentheformissubmitted,skipping
thebasketpageentirely.YoucanseetheeffectinFigure2-3.
38
www.it-ebooks.info
CHAPTER2GETTINGSTARTED
Figure2-3.Changingtheflowoftheapplication
Asthisexampledemonstrates,youcanchangetheflowofawebapplicationaswellasthe
appearanceandinteractivityofindividualpages.Ofcourse,theback-endservicesneedtounderstand
thevariouspathsthatdifferentkindsofusercanfollowthroughawebapp,butthisiseasytoachieve
withalittleforethoughtandplanning.
UnderstandingProgressiveEnhancement
ThetechniquesIhavedemonstratedinthischapterarebasicbutveryeffective.ByusingJavaScriptto
managetheelementsintheDOMandrespondtoevents,Ihavebeenabletomaketheexamplewebapp
moreresponsivefortheuser,provideusefulandtimelyinformationaboutthecostoftheuser’sproduct
selections,andstreamlinetheflowoftheappitself.
But—andthisisimportant—becausethesechangesaredonethroughJavaScript,thebasicnature
andstructureofthewebappremainunchangedfornon-JavaScriptusers.Figure2-4showsthemain
webapppagewhenJavaScriptisenabledanddisabled.
39
www.it-ebooks.info
CHAPTER2GETTINGSTARTED
Figure2-4.ThewebappasshownwhenJavaScriptisdisabledandenabled
Theversionthatnon-JavaScriptusersexperienceremainsfullyfunctionalbutisclunkiertouseand
requiresmorestepstoplaceanorder.
Creatingabaseleveloffunctionalityandthenselectivelyenrichingitisanexampleofprogressive
enhancement.Progressiveenhancementisn’tjustabouttheavailabilityofJavaScript;itencompasses
selectiveenrichmentbasedonanyfactor,suchastheamountofbandwidth,thetypeofbrowser,oreven
thelevelofexperienceoftheuser.However,whencreatingwebapps,themostcommonformof
progressiveenhancementisdrivenbywhethertheuserhasJavaScriptenabled.
TipAsimilartermtoprogressiveenhancementisgracefuldegradation.Formypurposesinthisbook,
progressiveenhancementandgracefuldegradationarethesame—thenotionthatthecorecontentandfeatures
ofawebapplicationareavailabletoallusers,irrespectiveofthecapabilitiesofauser’sbrowser.
Ifyoudon’twanttosupportnon-JavaScriptbrowsers,thenyoushouldmakeitobvioustononJavaScriptvisitorsthatthereisaproblem.Theeasiestwaytodothisisbyusingthenoscriptandmeta
elementstoredirectthebrowsertoapagethatexplainsthesituation,asshowninListing2-17.
Listing2-17.DealingwithNon-JavaScriptUsers
...
<head>
<title>CheeseLux</title>
<link rel="stylesheet" type="text/css" href="styles.css"/>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script>
... JavaScript code goes here...
40
www.it-ebooks.info
CHAPTER2GETTINGSTARTED
</script>
<noscript>
<meta http-equiv="refresh" content="0; noscript.html"/>
</noscript>
</head>
...
Thiscombinationofelementsredirectstheusertoapagecallednoscript.html,whichisanHTML
documentthattellstheuserthatIrequireJavaScript(and,obviously,doesn’trelyonJavaScriptitself).
Youcanfindthispageinthesourcecodedownloadthataccompaniesthisbookandseetheresultin
Figure2-5.
Figure2-5.EnforcingaJavaScript-onlypolicyinawebapp
ItistemptingtorequireJavaScript,butIrecommendcaution;youmightbesurprisedbyhowmany
usersdon’tenableJavaScriptorsimplycan’t.Thisisespeciallytrueforusersinlargecorporations,
wherecomputersareusuallylockeddownandwherefeaturesthatarecommoninthegeneral
populationaredisabledinthenameofsecurity,including,sadly,JavaScriptinbrowsers.Somewebapps
justdon’tmakesensewithoutJavaScript,butgivecarefulthoughttothepotentialusers/customersyou
willbeexcludingbeforedecidingthatyouarebuildingoneofthem.
NoteThisisabookaboutbuildingwebappswithJavaScript,soIamnotgoingtomaintainprogressive
enhancementinthechaptersthatfollow.Don’ttakethatasanendorsementofaJavaScript-onlypolicy.Inmyown
projects,Itrytosupportnon-JavaScriptuserswheneverpossible,evenwhenitrequiresalotofadditionaleffort.
41
www.it-ebooks.info
CHAPTER2GETTINGSTARTED
RevisitingtheButton:UsingaUIToolkit
Iwanttofinishthischapterbyshowingyouadifferentapproachtoobtainingoneoftheresultsinthis
chapter:creatingavisuallyconsistentbutton.ThetechniquesIusedpreviouslydemonstratedhowyou
canmanipulatetheDOMandrespondtoeventstotailortheappearanceandbehaviorofelements,
whichisthemainpremiseinthischapter.
Thatsaid,forprofessionaldevelopment,itagoodprincipletoneverwritewhatyoucanobtainfrom
agoodJavaScriptlibrary,andwhenIwanttocreatevisuallyrichelements,IuseaUItoolkit.Inthis
section,I’llshowyouhoweasyitistocreateacustombuttonwithjQueryUI,whichisproducedbythe
jQueryteamandisoneofthemostwidelyusedJavaScriptUItoolkitsavailable.
SettingUpjQueryUI
SettingupjQueryUIisamultistageprocess.Thefirststageistocreateatheme,whichdefinestheCSS
stylesthatareusedbythejQueryUIwidgets(whichisthenamegiventothestyledelementsthataUI
toolkitcreates).Tocreateatheme,gotohttp://jqueryui.com,clicktheThemesbutton,expandeach
sectionontheleftsideofthescreen,andspecifythestylesyouwant.Asyoumakechanges,thesample
widgetsontherightsideofthescreenwillupdatetoreflectthenewsettings.Ittookmeaboutfive
minutes(andabitoftrialanderror)tocreateathemethatmatchestheappearanceoftheexampleweb
app.IhaveincludedthethemeIcreatedinthesourcecodedownloadforthisbookifyoudon’twantto
createyourown.
TipIfyoudon’twanttocreateacustomtheme,youcanselectapredefinedstylefromthegallery.Thiscanbe
usefulifyouarenottryingtomatchanexistingappdesign,althoughthecolorsusedinsomeofgallerystylesare
quitealarming.
Whenyouaredone,clicktheDownloadThemebutton.Youwillseeascreenthatallowsyouto
selectwhichcomponentsofjQueryUIareincludedinthedownload.Youcancreateasmallerdownload
ifyougetintothedetailofjQueryUI,butforthisbookensurethatallofthecomponentsareselected
andclicktheDownloadbutton.Yourbrowserwilldownloada.zipfilethatcontainsthejQueryUI
library,theCSSthemeyoucreated,andsomesupportingimages.
Thesecondpartofthesetupistocopythefollowingfilesfromthe.zipfileintothecontent
directoryoftheNode.jsserver:
•
Thedevelopment-bundle\ui\jquery-ui-1.8.16.custom.jsfile
•
Thedevelopment-bundle\themes\custom-theme\jquery-ui-1.8.16.custom.cssfile
•
Thedevelopment-bundle\themes\custom-theme\imagesfolder
ThenamesofthefilesincludethejQueryUIversionnumbers.AsIwritethis,thecurrentversionis
1.8.16,butyouwillprobablyhavealaterversionbythetimethisbookgoesintoprint.
42
www.it-ebooks.info
CHAPTER2GETTINGSTARTED
TipOnceagain,IamusingtheuncompressedversionsoftheJavaScriptfiletomakedebuggingeasier.You
willfindtheminimizedversioninthejsfolderofthe.zipfile.
CreatingajQueryUIButton
NowthatjQueryUIissetup,IcanuseitinmyHTMLdocumenttocreateabuttonwidgetandsimplify
mycode.Listing2-18showstheadditionsrequiredtoimportjQueryUIintothedocumentandtocreate
abutton.
ImportingjQueryUIissimplyamatterofaddingascriptelementtoimporttheJavaScriptfileand
alinkelementtoimporttheCSSfile.Youdon’tneedtoexplicitlyreferencetheimagesdirectory.
TipNoticethatthescriptelementthatimportsthejQueryUIJavaScriptfilecomesaftertheonethatimports
jQuery.ThisorderingisimportantsincejQueryUIdependsonjQuery.
Listing2-18.UsingjQueryUItoCreateaButton
<!DOCTYPE html>
<html>
<head>
<title>CheeseLux</title>
<link rel="stylesheet" type="text/css" href="styles.css"/>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script src="jquery-ui-1.8.16.custom.js" type="text/javascript"></script>
<link rel="stylesheet" type="text/css" href="jquery-ui-1.8.16.custom.css"/>
<script>
var priceData = {
camembert: 18,
tomme: 19,
morbier: 9
}
$(document).ready(function() {
$('#buttonDiv input:submit').button().css("font-family", "Yanone, sans-serif");
$('.latent').show();
$('input').bind("change keyup", function() {
var subtotal = $(this).val() * priceData[this.name];
$(this).siblings("span").children("span").text(subtotal)
calculateTotal();
})
43
www.it-ebooks.info
CHAPTER2GETTINGSTARTED
$('form').attr("action", "/shipping");
})
function calculateTotal() {
var total = 0;
$('span.subtotal span').not('#total').each(function(index, elem) {
total += Number($(elem).text());
})
$('#total').text("$" + total);
}
</script>
<noscript>
<meta http-equiv="refresh" content="0; noscript.html"/>
</noscript>
</head>
...
WhenusingjQueryUI,Idon’thavetohidetheinputelementandinsertasubstitute.Instead,Iuse
jQuerytoselecttheelementIwanttomodifyandcallthebuttonmethod,asfollows:
$('#buttonDiv input:submit').button()
Withasinglemethodcall,jQueryUIchangestheappearanceofthelabelsandhandlesthe
highlightingwhenthemousehoversoverthebutton.Idon’tneedtoworryabouthandlingtheclick
eventinthiscase,becausethedefaultactionforasubmitinputelementistosubmittheform,whichis
exactlywhatIwanttohappen.
Ihavemadeoneadditionalmethodcall,usingthecssmethod.ThismethodappliesaCSSproperty
directlytotheselectedelementsusingthestyleattribute,andIhaveusedittosetthefont-family
propertyontheinputelement.ThejQueryUIthemesystemdoesn’thavemuchsupportfordealingwith
fontsandgeneratesitswidgetsusingasinglefontfamily.IhavesetupwebfontsfromtheGoogleFonts
(www.google.com/webfontsandtheexcellentLeagueofMovableType
(www.theleagueofmoveabletype.com),soImustoverridethejQueryUICSSstylestoapplymypreferred
fonttothebuttonelement.YoucanseetheresultofusingjQueryUItocreateabuttoninFigure2-6.The
resultis,asyoucansee,consistentwiththerestofthewebappbutmuchsimplertocreateinJavaScript.
Figure2-6.CreatingabuttonwithjQueryUI
ToolkitslikejQueryUIarejustaconvenientwrapperaroundthesameDOM,CSS,andevent
techniquesIdescribedearlier.Itisimportanttounderstandwhat’shappeningunderthecovers,butI
recommendusingjQueryUIoranothergoodUIlibrary.Theselibrariesarecomprehensivelytested,and
theysaveyoufromhavingtowriteanddebugcustomcode,allowingyoutospendmoretimeonthe
featuresthatsetyourwebappapartfromthecompetition.
44
www.it-ebooks.info
CHAPTER2GETTINGSTARTED
Summary
AsImentionedatthestartofthischapter,thetechniquesIusedintheseexamplesaresimple,reliable,
andentirelysuitedtosmallwebapps.Thereisnothingintrinsicallywrongwithusingtheseapproaches
iftheappissosmallthattherecanneverbeanyissueaboutmaintainingitbecauseeveryaspectofits
behaviorisimmediatelyobvioustoaprogrammer.
However,ifyouarereadingthisbook,youwanttogofurtherandcreatewebappsthatarelarge,are
complex,andhavemanymovingparts.Andwhenappliedtosuchwebapps,thesetechniquescreate
somefundamentalproblems.Theunderlyingissueisthatthedifferentaspectsofthewebappareall
mixedtogether.Theapplicationdata(theproductsandthebasket),thepresentationofthatdata(the
HTMLelements),andtheinteractionsbetweenthem(theJavaScripteventsandhandlerfunctions)are
distributedthroughoutthedocument.Thismakesithardtoaddadditionaldata,extendthe
functionality,orfixbugswithoutintroducingerrors.
Inthechaptersthatfollow,Ishowyouhowtoapplyheavy-dutytechniquesfromtheworldof
server-sidedevelopmenttothewebapp.Client-sidedevelopmenthasbeenthepoorcousinofserversideworkformanyyears,butasbrowsersbecomemorecapable(andaswebappprogrammersbecome
moreambitious),wecannolongerpretendthattheclientsideisanythingotherthanafull-fledged
platforminitsownright.Itistimetotakewebappdevelopmentseriously,andinthechaptersthat
follow,Ishowyouhowtocreateasolid,robust,andscalablefoundationforyourwebapp.
45
www.it-ebooks.info
CHAPTER 3
Adding a View Model
Ifyouhavedoneanyseriousdesktoporserver-sidedevelopment,youwillhaveencounteredeitherthe
Model-View-Controller(MVC)designpatternoritsderivativeModel-View-View-Model(MVVM).Iam
notgoingtodescribeeitherpatterninanydetail,otherthantosaythatthecoreconceptinbothis
separatingthedata,operations,andpresentationofanapplicationintoseparatecomponents.
Thereisalotofbenefitinapplyingthesamebasicprinciplestoawebapplication.Iamnotgoingto
getboggeddowninthedesignpatternsandterminology.Instead,Iamgoingtofocusondemonstrating
theprocessforstructuringawebappandexplainingthebenefitsthataregainedfromdoingso.
ResettingtheExample
Thebestwaytounderstandhowtoapplyaviewmodelandthebenefitsthatdoingsoconfersisto
simplydoit.ThefirstthingtodoiscuteverythingbutthebasicsoutoftheapplicationsothatIhavea
cleanslatetostartfrom.AsyoucanseeinListing3-1,Ihaveremovedeverythingbutthebasicstructure
ofthedocument.
Listing3-1.WipingtheSlate
<!DOCTYPE html>
<html>
<head>
<title>CheeseLux</title>
<link rel="stylesheet" type="text/css" href="styles.css"/>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script src="jquery-ui-1.8.16.custom.js" type="text/javascript"></script>
<link rel="stylesheet" type="text/css" href="jquery-ui-1.8.16.custom.css"/>
<noscript>
<meta http-equiv="refresh" content="0; noscript.html"/>
</noscript>
<script>
$(document).ready(function() {
$('#buttonDiv input:submit').button().css("font-family", "Yanone");
})
</script>
</head>
<body>
<div id="logobar">
<img src="cheeselux.png">
<span id="tagline">Gourmet European Cheese</span>
47
www.it-ebooks.info
CHAPTER3ADDINGAVIEWMODEL
</div>
<form action="/shipping" method="post">
<div id="buttonDiv">
<input type="submit" />
</div>
</form>
</body>
</html>
CreatingaViewModel
Thenextstepistodefinesomedata,whichwillbethefoundationoftheviewmodel.Togetstarted,I
haveaddedanobjectthatdescribestheproductsinthecheeseshop,asshowninListing3-2.
Listing3-2.AddingDatatotheDocument
<script>
var cheeseModel = {
category: "French Cheese",
items: [ {id: "camembert", name: "Camembert", price: 18},
{id: "tomme", name: "Tomme de Savoie", price: 19},
{id: "morbier", name: "Morbier", price: 9}]
};
$(document).ready(function() {
$('#buttonDiv input:submit').button().css("font-family", "Yanone");
});
</script>
Ihavecreatedanobjectthatcontainsdetailsofthecheeseproductsandassignedittoavariable
calledcheeseModel.TheobjectdescribesthesameproductsthatIusedChapter2andisthefoundation
ofmyviewmodel,whichIwillbuildthroughoutthechapter;itisasimpledataobjectnow,butI’llbe
doingalotmorewithitsoon.
TipIfyoufindyourselfstaringattheblinkingcursorwithnorealideahowtodefineyourapplicationdata,then
myadviceissimple:juststarttyping.Oneofthebiggestbenefitsofembracingaviewmodelisthatitmakes
changeseasier,andthatincludeschangestothestructureoftheunderlyingdata.Don’tworryifyoudon’tgetit
right,becauseyoucanalwayscorrectitlater.
48
www.it-ebooks.info
CHAPTER3ADDINGAVIEWMODEL
AdoptingaViewModelLibrary
FollowingtheprincipleofnotwritingwhatisavailableinagoodJavaScriptlibrary,Iwillintroducea
viewmodelintothewebappusingaviewmodellibrary.TheoneI’llbeusingiscalledKnockout(KO).I
liketheKOapproachtoapplicationstructure,andthemainprogrammerforKOisSteveSanderson,who
ismycoauthorfortheProASP.NETMVCbookfromApressandanall-aroundniceguy.TogetKO,goto
http://knockoutjs.comandclicktheDownloadlink.Selectthemostrecentversion(whichis2.0.0asI
writethis)fromthelistoffilesandcopyittotheNode.jscontentdirectory.
TipDon’tworryifyoudon’tgetonwithKO.Otherstructurelibrariesareavailable.Themaincompetitioncomes
fromBackbone(http://documentcloud.github.com/backbone)andAngularJS(http://angularjs.org).The
implementationdetailsinthesealternativelibrariesmaydiffer,buttheunderlyingprinciplesremainthesame.
Inthesectionsthatfollow,Iwillbringmyviewmodelandtheviewmodellibrarytogetherto
decouplepartsoftheexampleapplication.
GeneratingContentfromtheViewModel
Tobegin,IamgoingtousethedatatogenerateelementsinthedocumentsothatIcandisplaythe
productstotheuser.Thisisasimpleuseoftheviewmodel,butitreproducesthebasicfunctionalityof
theimplementationinChapter2andgivesmeagoodfoundationfortherestofthechapter.Listing3-3
showstheadditionoftheKOlibrarytothedocumentandthegenerationoftheelementsfromthedata.
Listing3-3.GeneratingElementsfromtheViewModel
<!DOCTYPE html>
<html>
<head>
<title>CheeseLux</title>
<link rel="stylesheet" type="text/css" href="styles.css"/>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script src="jquery-ui-1.8.16.custom.js" type="text/javascript"></script>
<script src='knockout-2.0.0.js' type='text/javascript'></script>
<link rel="stylesheet" type="text/css" href="jquery-ui-1.8.16.custom.css"/>
<noscript>
<meta http-equiv="refresh" content="0; noscript.html"/>
</noscript>
<script>
var cheeseModel = {
category: "French Cheese",
items: [ {id: "camembert", name: "Camembert", price: 18},
{id: "tomme", name: "Tomme de Savoie", price: 19},
{id: "morbier", name: "Morbier", price: 9}]
};
$(document).ready(function() {
49
www.it-ebooks.info
CHAPTER3ADDINGAVIEWMODEL
$('#buttonDiv input:submit').button().css("font-family", "Yanone");
ko.applyBindings(cheeseModel);
});
</script>
</head>
<body>
<div id="logobar">
<img src="cheeselux.png">
<span id="tagline">Gourmet European Cheese</span>
</div>
<form action="/shipping" method="post">
<div class="cheesegroup">
<div class="grouptitle" data-bind="text: category"></div>
<div data-bind="foreach: items">
<div class="groupcontent">
<label data-bind="attr: {for: id}" class="cheesename">
<span data-bind="text: name">
</span> $(<span data-bind="text:price"></span>)</label>
<input data-bind="attr: {name: id}" value="0"/>
</div>
</div>
</div>
<div id="buttonDiv">
<input type="submit" />
</div>
</form>
</body>
</html>
Therearethreesetsofadditionsinthislisting.ThefirstisimportingtheKOJavaScriptlibraryinto
thedocumentwithascriptelement.ThesecondadditiontellsKOtousemyviewmodelobject:
ko.applyBindings(cheeseModel);
ThekoobjectisthegatewaytotheKOlibraryfunctionality,andtheapplyBindingsmethodtakesthe
viewmodelobjectasanargumentandusesit,asthenamesuggests,tofulfillthebindingsdefinedinthe
document;thesearethethirdsetofadditions.YoucanseetheresultofthesebindingsinFigure3-1,and
Iexplainhowtheyworkinthesectionsthatfollow.
50
www.it-ebooks.info
CHAPTER3ADDINGAVIEWMODEL
Figure3-1.Creatingcontentfromtheviewmodel
UnderstandingValueBindings
AvaluebindingisarelationshipbetweenapropertyintheviewmodelandanHTMLelement.Thisis
thesimplestkindofbindingavailable.HereisanexampleofanHTMLelementthathasavaluebinding:
<div class="grouptitle" data-bind="text: category"></div>
AllKObindingsaredefinedusingthedata-bindattribute.Thisisanexampleofatextbinding,
whichhastheeffectofsettingthetextcontentoftheHTMLelementtothevalueofthespecifiedview
modelproperty,inthiscase,thecategoryproperty.
WhentheapplyBindingsmethodiscalled,KOsearchesforbindingsandinsertstheappropriate
datavalueintothedocument,transformingtheelementlikethis:
<div class="grouptitle" data-bind="text: category">French Cheese</div>
TipIlikehavingtheKOdatabindingsdefinedintheelementswheretheywillbeapplied,butsomepeople
don’tlikethisapproach.ThereisasimplelibraryavailablethatsupportsunobtrusiveKOdatabindings,meaning
thatthebindingsaresetupusingjQueryinthescriptelement.Youcangetthecodeandseeanexampleat
https://gist.github.com/1006808.
51
www.it-ebooks.info
CHAPTER3ADDINGAVIEWMODEL
TheotherbindingIusedinthisexamplewasattr,whichsetsthevalueofanelementattributetoa
propertyfromthemodel.Hereisanexampleofanattrbindingfromthelisting:
<input data-bind="attr: {name: id}" value="0"/>
ThisbindingspecifiesthatKOshouldinsertthevalueoftheidpropertyforthenameattribute,which
producesthefollowingresultwhenthebindingsareapplied:
<input data-bind="attr: {name: id}" value="0" name="camembert">
KOvaluebindingsdon’tsupportanyformattingorcombiningofvalues.Infact,valuebindingsjust
insertasinglevalueintothedocument,andthatmeansthatextraelementsareoftenneededastargets
forvaluebindings.Youcanseethisinthelabelelementinthelisting,whereIaddedacoupleofspan
elements:
<label data-bind="attr: {for: id}" class="cheesename">
<span data-bind="text: name"></span> $(<span data-bind="text:price"></span>)
</label>
Iwantedtoinserttwodatavaluesasthecontentforthelabelelementwithsomesurrounding
characterstoindicatecurrency.Thewaytogetthedesiredeffectissimpleenough,albeititaddssome
complexitytotheHTMLstructure.Analternativeistocreatecustombindings,whichIexplainin
Chapter4.
TipThetextandattrbindingsarethemostuseful,butKOsupportsotherkindsofvaluebindingsaswell:
visible,html,css,andstyle.IusethevisiblebindinglaterinthechapterandthecssbindinginChapter4,
butyoushouldconsulttheKOdocumentationatknockoutjs.comfordetailsoftheothers.
UnderstandingFlowControlBindings
Flowcontrolbindingsprovidethemeanstousetheviewmodeltocontrolwhichelementsareincluded
inthedocument.Inthelisting,Iusedtheforeachbindingtoenumeratetheitemsviewmodelproperty.
Theforeachbindingisusedonviewmodelpropertiesthatarearraysandduplicatesthesetofchild
elementsforeachiteminthearray:
<div data-bind="foreach: items">
...
</div>
Valuebindingsonthechildelementscanrefertothepropertiesoftheindividualarrayitems,which
ishowIamabletospecifytheidpropertyfortheattrbindingontheinputelement:KOknowswhich
arrayitemisbeingprocessedandinsertstheappropriatevaluefromthatitem.
52
www.it-ebooks.info
CHAPTER3ADDINGAVIEWMODEL
TipInadditiontotheforeachbinding,KOalsosupportstheif,ifnot,andwithbindings,whichallowcontent
tobeselectivelyincludedinorexcludedfromadocument.Idescribetheifandifnotbindingslaterinthis
chapter,butyoushouldconsulttheKOdocumentationatknockoutjs.comforfulldetails.
TakingAdvantageoftheViewModel
NowthatIhavethebasicstructureoftheapplicationinplace,IcanusetheviewmodelandKOtodo
more.Iwillstartwithsomebasicfeatureandthenstepthingsuptoshowyousomemoreadvanced
techniques.
AddingMoreProductstotheViewModel
Thefirstbenefitthataviewmodelbringsistheabilitytomakechangesmorequicklyandwithfewer
errorsthanwouldotherwisebepossible.Thesimplestdemonstrationofthisistoaddmoreproductsto
thecheeseshopcatalog.Listing3-4showsthechangesrequiredtoaddcheesesfromothercountries.
Listing3-4.AddingtotheViewModel
<!DOCTYPE html>
<html>
<head>
<title>CheeseLux</title>
<link rel="stylesheet" type="text/css" href="styles.css"/>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script src="jquery-ui-1.8.16.custom.js" type="text/javascript"></script>
<script src='knockout-2.0.0.js' type='text/javascript'></script>
<link rel="stylesheet" type="text/css" href="jquery-ui-1.8.16.custom.css"/>
<noscript>
<meta http-equiv="refresh" content="0; noscript.html"/>
</noscript>
<script>
var cheeseModel = {
products: [
{category: "British Cheese", items : [
{id: "stilton", name: "Stilton", price: 9},
{id: "stinkingbishop", name: "Stinking Bishop", price: 17},
{id: "cheddar", name: "Cheddar", price: 17}]},
{category: "French Cheese", items: [
{id: "camembert", name: "Camembert", price: 18},
{id: "tomme", name: "Tomme de Savoie", price: 19},
{id: "morbier", name: "Morbier", price: 9}]},
{category: "Italian Cheese", items: [
{id: "gorgonzola", name: "Gorgonzola", price: 8},
{id: "fontina", name: "Fontina", price: 11},
53
www.it-ebooks.info
CHAPTER3ADDINGAVIEWMODEL
{id: "parmesan", name: "Parmesan", price: 16}]}]
};
$(document).ready(function() {
$('#buttonDiv input:submit').button().css("font-family", "Yanone");
ko.applyBindings(cheeseModel);
});
</script>
</head>
<body>
<div id="logobar">
<img src="cheeselux.png">
<span id="tagline">Gourmet European Cheese</span>
</div>
<form action="/shipping" method="post">
<div data-bind="foreach: products">
<div class="cheesegroup">
<div class="grouptitle" data-bind="text: category"></div>
<div data-bind="foreach: items">
<div class="groupcontent">
<label data-bind="attr: {for: id}" class="cheesename">
<span data-bind="text: name">
</span> $(<span data-bind="text:price"></span>)</label>
<input data-bind="attr: {name: id}" value="0"/>
</div>
</div>
</div>
</div>
<div id="buttonDiv">
<input type="submit" />
</div>
</form>
</body>
</html>
Thebiggestchangewastotheviewmodelitself.Ichangedthestructureofthedataobjectsothat
eachcategoryofproductsisanelementinanarrayassignedtotheproductsproperty(and,ofcourse,I
addedtwonewcategories).IntermsoftheHTMLcontent,Ijusthadtoaddaforeachflowcontrol
bindingsothattheelementscontainedwithinareduplicatedforeachcategory.
54
www.it-ebooks.info
CHAPTER3ADDINGAVIEWMODEL
TipTheresultoftheseadditionsisalong,thinHTMLdocument.Thisisnotanidealwayofdisplayingdata,but
asIsaidinChapter1,thisisabookaboutadvancedprogrammingandnotabookaboutdesign.Therearelotsof
waystopresentthisdatamoreusefully,andIsuggeststartingbylookingatthetabswidgetsofferedbyUItoolkits
suchasjQueryUIorjQueryTools.
CreatingObservableDataItems
Inthepreviousexample,IusedKOlikeasimpletemplateengine;Itookthevaluesfromtheviewmodel
andusedthemtogenerateasetofelements.Ilikeusingtemplateenginesbecausetheysimplifymarkup
andreduceerrors.Butabiggerbenefitofviewmodelscomeswhenyoucreateobservabledataitems.Put
simply,anobservabledataitemisapropertyintheviewmodelthat,whenupdated,causesallofthe
HTMLelementsthathavevaluebindingstothatpropertytoupdateaswell.Listing3-5showshowto
createanduseanobservabledataitem.
Listing3-5.CreatingObservableDataItems
<!DOCTYPE html>
<html>
<head>
<title>CheeseLux</title>
<link rel="stylesheet" type="text/css" href="styles.css"/>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script src="jquery-ui-1.8.16.custom.js" type="text/javascript"></script>
<script src='knockout-2.0.0.js' type='text/javascript'></script>
<link rel="stylesheet" type="text/css" href="jquery-ui-1.8.16.custom.css"/>
<noscript>
<meta http-equiv="refresh" content="0; noscript.html"/>
</noscript>
<script>
var cheeseModel = {
products: [
{category: "British Cheese", items : [
{id: "stilton", name: "Stilton", price: 9},
{id: "stinkingbishop", name: "Stinking Bishop", price: 17},
{id: "cheddar", name: "Cheddar", price: 17}]},
{category: "French Cheese", items: [
{id: "camembert", name: "Camembert", price: 18},
{id: "tomme", name: "Tomme de Savoie", price: 19},
{id: "morbier", name: "Morbier", price: 9}]},
{category: "Italian Cheese", items: [
{id: "gorgonzola", name: "Gorgonzola", price: 8},
{id: "fontina", name: "Fontina", price: 11},
{id: "parmesan", name: "Parmesan", price: 16}]}]
};
55
www.it-ebooks.info
CHAPTER3ADDINGAVIEWMODEL
function mapProducts(func) {
$.each(cheeseModel.products, function(catIndex, outerItem) {
$.each(outerItem.items, function(itemIndex, innerItem) {
func(innerItem);
});
});
}
$(document).ready(function() {
$('#buttonDiv input').button().css("font-family", "Yanone");
mapProducts(function(item) {
item.price = ko.observable(item.price);
});
ko.applyBindings(cheeseModel);
$('#discount').click(function() {
mapProducts(function(item) {
item.price(item.price() - 2);
});
});
});
</script>
</head>
<body>
<div id="logobar">
<img src="cheeselux.png">
<span id="tagline">Gourmet European Cheese</span>
</div>
<form action="/shipping" method="post">
<div id="buttonDiv">
<input id="discount" type="button" value="Apply Discount" />
</div>
<div data-bind="foreach: products">
<div class="cheesegroup">
<div class="grouptitle" data-bind="text: category"></div>
<div data-bind="foreach: items">
<div class="groupcontent">
<label data-bind="attr: {for: id}" class="cheesename">
<span data-bind="text: name">
</span> $(<span data-bind="text:price"></span>)</label>
<input data-bind="attr: {name: id}" value="0"/>
</div>
</div>
</div>
</div>
56
www.it-ebooks.info
CHAPTER3ADDINGAVIEWMODEL
<div id="buttonDiv">
<input type="submit" />
</div>
</form>
</body>
</html>
ThemapProductsfunctionisasimpleutilitythatallowsmetoapplyafunctiontoeachindividual
cheeseproduct.ThisfunctionusesthejQueryeachmethod,whichexecutesafunctionforeveryitemin
anarray.Byusingtheeachfunctiontwice,Icanreachtheinnerarrayofcheeseproductsineach
category.
Inthisexample,Ihavetransformedthepricepropertyforeachcheeseproductintoanobservable
dataitem,asfollows:
mapProducts(function(item) {
item.price = ko.observable(item.price);
});
Theko.observablemethodtakestheinitialvalueforthedataitemasitsargumentandsetsupthe
plumbingthatisrequiredtodisseminateupdatestothebindingsinthedocument.Idon’thavetomake
anychangestothebindingsthemselves;KOtakescareofallthedetailsforme.
Allthatremainsistosetupasituationthatwillcauseachangetooccur.Ihavedonethisbyaddinga
newbuttontothedocumentanddefiningahandlerfortheclickeventasfollows:
$('#discount').click(function() {
mapProducts(function(item) {
item.price(item.price() - 2);
});
});
Whenthebuttonisclicked,IusethemapProductsfunctiontochangethevalueofthepriceproperty
foreachcheeseobjectintheviewmodel.Sincethisisanobservabledataitem,thenewvaluewillbe
pushedouttothevaluebindingsandcausethedocumenttobeupdated.
NoticetheslightlyoddsyntaxIusewhenalteringthevalue.Theoriginalpricepropertywasa
JavaScriptNumber,whichmeantIcouldchangethevaluelikethis:
item.price -= 2;
Buttheko.observablemethodtransformsthepropertyintoaJavaScriptfunctioninordertowork
withsomeolderversionsofInternetExplorer.Thismeansyoureadthevalueofanobservabledataitem
bycallingthefunction(inotherwords,bycallingitem.price())andupdatethevaluebypassingan
argumenttothefunction(inotherwords,bycallingitem.price(newValue)).Thiscantakealittlewhileto
getusedto,andIstillforgettodothis.
Figure3-2showstheeffectoftheobservabledataitem.WhentheApplyDiscountbuttonisclicked,
allofthepricesdisplayedtotheuserareupdated,asFigure3-2shows.
57
www.it-ebooks.info
CHAPTER3ADDINGAVIEWMODEL
Figure3-2.Usinganobservabledataitem
Thepowerandflexibilityofanobservabledataitemissignificant;itcreatesanapplicationwhere
changesfromtheviewmode,irrespectiveofhowtheyarise,causethedatabindingsinthedocumentto
beupdatedimmediately.Asyou’llseeintherestofthechapter,Imakealotofuseofobservabledata
itemsasIaddmorecomplexfeaturestotheexamplewebapp.
CreatingBidirectionalBindings
Abidirectionalbindingisatwo-wayrelationshipbetweenaformelementandanobservabledataitem.
Whentheviewmodelisupdated,soisthevalueshownintheelement,justasforaregularobservable.In
addition,changingtheelementvaluecausesanupdatetogointheotherdirection:thepropertyinthe
viewmodelisupdated.So,forexample,ifIuseabidirectionalbindingforaninputelement,KOensures
thatthemodelisupdatedwhentheuserentersanewvalue.Byusingbidirectionalrelationships
betweenmultipleelementsandthesamemodelproperty,youcaneasilykeepacomplexwebapp
synchronizedandconsistent.
Todemonstrateabidirectionalbinding,IwilladdaSpecialOfferssectiontothecheeseshop.This
allowsmetopicksomeproductsfromthefullsection,applyadiscount,and,ideally,drawthe
customer’sattentiontoaproductthattheymightnototherwiseconsider.
Listing3-6containsthechangestothewebapptosupportthespecialoffers.Tosetupa
bidirectionalbinding,Iamgoingtodotwootherinterestingthings:extendtheviewmodelanduseKO
templatestogenerateelements.I’llexplainallthreechangesinthesectionsthatfollowthelisting.
58
www.it-ebooks.info
CHAPTER3ADDINGAVIEWMODEL
Listing3-6.UsingLiveBindingstoCreateSpecialOffers
<!DOCTYPE html>
<html>
<head>
<title>CheeseLux</title>
<link rel="stylesheet" type="text/css" href="styles.css"/>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script src="jquery-ui-1.8.16.custom.js" type="text/javascript"></script>
<script src='knockout-2.0.0.js' type='text/javascript'></script>
<link rel="stylesheet" type="text/css" href="jquery-ui-1.8.16.custom.css"/>
<noscript>
<meta http-equiv="refresh" content="0; noscript.html"/>
</noscript>
<script>
var cheeseModel = {
products: [
{category: "British Cheese", items : [
{id: "stilton", name: "Stilton", price: 9},
{id: "stinkingbishop", name: "Stinking Bishop", price: 17},
{id: "cheddar", name: "Cheddar", price: 17}]},
{category: "French Cheese", items: [
{id: "camembert", name: "Camembert", price: 18},
{id: "tomme", name: "Tomme de Savoie", price: 19},
{id: "morbier", name: "Morbier", price: 9}]},
{category: "Italian Cheese", items: [
{id: "gorgonzola", name: "Gorgonzola", price: 8},
{id: "fontina", name: "Fontina", price: 11},
{id: "parmesan", name: "Parmesan", price: 16}]}]
};
function mapProducts(func) {
$.each(cheeseModel.products, function(catIndex, outerItem) {
$.each(outerItem.items, function(itemIndex, innerItem) {
func(innerItem);
});
});
}
$(document).ready(function() {
$('#buttonDiv input:submit').button().css("font-family", "Yanone");
cheeseModel.specials = {
category: "Special Offers",
discount: 3,
ids: ["stilton", "tomme"],
items: []
};
mapProducts(function(item) {
if ($.inArray(item.id, cheeseModel.specials.ids) > -1) {
59
www.it-ebooks.info
CHAPTER3ADDINGAVIEWMODEL
item.price -= cheeseModel.specials.discount;
cheeseModel.specials.items.push(item);
});
}
item.quantity = ko.observable(0);
ko.applyBindings(cheeseModel);
});
</script>
<script id="categoryTmpl" type="text/html">
<div class="cheesegroup">
<div class="grouptitle" data-bind="text: category"></div>
<div data-bind="foreach: items">
<div class="groupcontent">
<label data-bind="attr: {for: id}" class="cheesename">
<span data-bind="text: name">
</span> $(<span data-bind="text:price"></span>)</label>
<input data-bind="attr: {name: id}, value: quantity"/>
</div>
</div>
</div>
</script>
</head>
<body>
<div id="logobar">
<img src="cheeselux.png">
<span id="tagline">Gourmet European Cheese</span>
</div>
<div data-bind="template: {name: 'categoryTmpl', data: specials}"></div>
<form action="/shipping" method="post">
<div data-bind="template: {name: 'categoryTmpl', foreach: products}"></div>
<div id="buttonDiv">
<input type="submit" />
</div>
</form>
</body>
</html>
ExtendingtheViewModel
JavaScript’sloosetypinganddynamicnaturemakesitidealforcreatingflexibleandadaptableview
models.Ilikebeingabletotaketheinitialdataandreshapeittocreatesomethingthatismoreclosely
tailoredtotheneedsofthewebapp,inthiscase,toaddsupportforspecialoffers.Tostartwith,Iadda
propertycalledspecialstotheviewmodel,definingitasanobjectthathascategoryanditems
propertiesliketherestofthemodelbutwithsomeusefuladditions:
60
www.it-ebooks.info
CHAPTER3ADDINGAVIEWMODEL
cheeseModel.specials = {
category: "Special Offers",
discount: 3,
ids: ["stilton", "tomme"],
items: []
};
ThediscountpropertyspecifiesthedollardiscountIwanttoapplytothespecialoffers,andtheids
propertycontainsanarrayoftheIDsofproductsthatwillbespecialoffers.
Thespecials.itemsarrayisemptywhenIfirstdefineit.Topopulatethearray,Ienumeratethe
productsarraytofindthoseproductsthatareinthespecials.idsarray,likethis:
mapProducts(function(item) {
if ($.inArray(item.id, cheeseModel.specials.ids) > -1) {
item.price -= cheeseModel.specials.discount;
cheeseModel.specials.items.push(item);
}
item.quantity = ko.observable(0);
});
IusetheinArraymethodtodeterminewhetherthecurrentitemintheiterationisoneofthosethat
willbeincludedasaspecialoffer.TheinArraymethodisanotherjQueryutility,anditreturnstheindex
ofanitemifitiscontainedwithinanarrayand-1ifitisnot.Thisisaquickandeasywayformetocheck
toseewhetherthecurrentitemisonethatIaminterestedinasaspecialoffer.
Ifanitemisonthespecialslist,thenIreducethevalueofthepricepropertybythediscount
amountandusethepushmethodtoinserttheitemintothespecials.itemsarray.
item.price -= cheeseModel.specials.discount;
cheeseModel.specials.items.push(item);
AfterIhaveiteratedthroughtheitemsintheviewmodel,thespecials.itemarraycontainsa
completesetoftheproductsthataretobediscounted,and,alongtheway,Ihavereducedeachoftheir
prices.
Inthisexample,Ihavemadethequantitypropertyintoanobservabledataitem:
item.quantity = ko.observable(0);
ThisisimportantbecauseIamgoingtodisplaymultipleinputelementsforthespecialoffers:one
elementintheoriginalcheesecategoryandanotherinanewSpecial OfferscategorythatIexplainin
thenextsection.Byusinganobservabledataitemandbidirectionalbindingsontheinputelements,I
caneasilymakesurethatthequantitiesenteredforacheeseareconsistentlydisplayed,irrespectiveof
whichinputelementisused.
GeneratingtheContent
Allthatremainsnowistogeneratethecontentfromtheviewmodel.Iwanttogeneratethesamesetof
elementsforthespecialoffersasfortheregularcategories,soIhaveusedtheKOtemplatefeature,which
allowsmetogeneratethesamesetofelementsatmultiplepointsinthedocument.Hereisthetemplate
fromthelisting:
<script id="categoryTmpl" type="text/html">
<div class="cheesegroup">
<div class="grouptitle" data-bind="text: category"></div>
61
www.it-ebooks.info
CHAPTER3ADDINGAVIEWMODEL
<div data-bind="foreach: items">
<div class="groupcontent">
<label data-bind="attr: {for: id}" class="cheesename">
<span data-bind="text: name">
</span> $(<span data-bind="text:price"></span>)</label>
<input data-bind="attr: {name: id}, value: quantity"/>
</div>
</div>
</div>
</script>
Thetemplateiscontainedinascriptelement.Thetypeattributeissettotext/html,which
preventsthebrowserfromexecutingthecontentasJavaScript.Mostofthebindingsinthetemplateare
thesametextandattrbindingsIusedinthepreviousexample.Theimportantadditionistotheinput
element,asfollows:
<input data-bind="attr: {name: id}, value: quantity"/>
Thedata-bindattributeforthiselementdefinestwobindings,separatedbyacomma.Thefirstisa
regularattrbinding,butthesecondisavaluebinding,whichisoneofthebidirectionalbindingsthat
KOdefines.Idon’thavetotakeanyactiontomakethevaluebindingbidirectional;KOtakescareofit
automatically.Inthislisting,Icreateatwo-waybindingtothequantityobservabledataitem.
Igeneratecontentfromthetemplateusingthetemplatebinding.Whenusingatemplate,KO
duplicatestheelementsthatitcontainsandinsertsthemaschildrenoftheelementthathasthe
templatebinding.TherearetwopointsinthedocumentwhereIusethetemplate,andtheyareslightly
different:
<div data-bind="template: {name: 'categoryTmpl', data: specials}"></div>
<form action="/shipping" method="post">
<div data-bind="template: {name: 'categoryTmpl', foreach: products}"></div>
<div id="buttonDiv">
<input type="submit" />
</div>
</form>
Whenusingthetemplatebinding,thenamepropertyspecifiestheidattributevalueofthetemplate
element.Ifyouwanttogenerateonlyonesetofelements,thenyoucanusethedatapropertytospecify
whichviewmodelpropertywillbeused.Iuseddatatospecifythespecialspropertyinthelisting,which
createsasectionofcontentformyspecial-offerproducts.
TipYoumustremembertoenclosetheidofthetemplateelementinquotes.Ifyoudon’t,KOwillfailquietly
withoutgeneratingelementsfromthetemplate.
Youcanusetheforeachpropertyifyouwanttogenerateasetofelementsforeachiteminanarray.
Ihavedonethisfortheregularproductcategoriesbyspecifyingtheproductsarray.Inthisway,Ican
applythetemplatetoeachelementinanarraytogeneratecontentconsistently.
62
www.it-ebooks.info
CHAPTER3ADDINGAVIEWMODEL
TipNoticethatthespecial-offerelementsareinsertedoutsidetheformelement.Theinputelementsforthe
special-offerproductswillhavethesamenameattributevalueasthecorrespondinginputelementintheregular
productcategory.Byinsertingthespecial-offerelementsoutsidetheform,Ipreventduplicateentriesfrombeing
senttotheserverwhentheformissubmitted.
ReviewingtheResult
NowthatIhaveexplainedeachofthechangesImadetosetupthebidirectionalbindings,itistimeto
lookattheresults,whichyoucanseeinFigure3-3.
Figure3-3.Theresultofextendingtheviewmodel,creatingalivebinding,andusingtemplates
Thisisgooddemonstrationofhowusingaviewmodelcansavetimeandreduceerrors.Ihave
applieda$3discounttotheSpecialOfferproducts,whichIdidbyalteringthevalueoftheprice
propertyintheviewmodel.Eventhoughthepricepropertyisnotobservable,thecombinationofthe
viewmodelandthetemplateensuresthatthecorrectpricesaredisplayedthroughoutthedocument
whentheelementsareinitiallygenerated.(YoucanseethatbothStiltonlistingsarepricedat$6,rather
thanthe$9originallyspecifiedbytheviewmodel.)
Thebidirectionalbindingisthemostinterestingandusefulfeatureinthisexample.Alloftheinput
elementshavebidirectionalbindingswiththeircorrespondingquantityproperty,andsincethereare
twoinputelementsinthedocumentforeachoftheSpecialOffercheeses,enteringavalueintoonewill
63
www.it-ebooks.info
CHAPTER3ADDINGAVIEWMODEL
immediatelycausethatvaluetobedisplayedintheother;youcanseethishashappenedfortheStilton
productinthefigure(butitisaneffectthatisbestexperiencedbyloadingtheexampleinthebrowser).
So,withverylittleeffort,Ihavebeenabletoenhancetheviewmodelandusethoseenhancements
tokeepaformconsistentandresponsive,whileaddingnewfeaturestotheapplication.Inthenext
section,I’llbuildontheseenhancementstocreateadynamicbasket,showingyousomeoftheother
benefitsthatcanarisefromaviewmodel.
TipIfyousubmitthisformtotheserver,theordersummarywillshowtheoriginal,undiscountedprice.Thisis,
ofcourse,becauseIappliedthediscountonlyinthebrowser.Inarealapplication,theserverwouldalsoneedto
knowaboutthespecialoffers,butIamgoingtoskipoverthis,sincethisbookfocusesonclient-sidedevelopment.
AddingaDynamicBasket
NowthatIhaveexplainedanddemonstratedhowchangesaredetectedandpropagatedwithvalueand
bidirectionalbindings,IcancompletetheexamplesothatallofthefunctionalitypresentinChapter2is
availabletotheuser.ThismeansIneedtoimplementadynamicshoppingbasket,whichIdointhe
sectionsthatfollow.
AddingSubtotals
Withaviewmodel,newfeaturescanbeaddedquickly.Thechangestoaddper-itemsubtotalsare
surprisinglysimple,althoughIneedtousesomeadditionalKOfeatures.First,Ineedtoenhancethe
viewmodel.Listing3-7highlightsthechangesinthescriptelementwithinthecalltothemapProduct
function.
Listing3-7.ExtendingtheViewModeltoSupportSubtotals
...
mapProducts(function(item) {
if ($.inArray(item.id, cheeseModel.specials.ids) > -1) {
item.price -= cheeseModel.specials.discount;
cheeseModel.specials.items.push(item);
}
item.quantity = ko.observable(0);
item.subtotal = ko.computed(function() {
return this.quantity() * this.price;
}, item);
});
...
Ihavecreatedwhatisknownasacomputedobservabledataitemforthesubtotalproperty.Thisis
likearegularobservableitem,exceptthatthevalueisproducedbyafunction,whichispassedasthe
firstargumenttotheko.computedmethod.Thesecondmethodisusedasthevalueofthethisvariable
whenthefunctionisexecuted;Ihavesetthistotheitemloopvariable.
64
www.it-ebooks.info
CHAPTER3ADDINGAVIEWMODEL
ThenicethingaboutthisfeatureisthatKOmanagesallofthedependencies,suchthatwhenmy
computedobservablefunctionreliesonaregularobservabledataitem,achangetotheregularitem
automaticallytriggersanupdateinthecomputedvalue.I’llusethisbehaviortomanagetheoveralltotal
laterinthischapter.
Next,Ineedtoaddsomeelementswithbindingstothetemplate,asshowninListing3-8.
Listing3-8.AddingElementstotheTemplatetoSupportSubtotals
<script id="categoryTmpl" type="text/html">
<div class="cheesegroup">
<div class="grouptitle" data-bind="text: category"></div>
<div data-bind="foreach: items">
<div class="groupcontent">
<label data-bind="attr: {for: id}" class="cheesename">
<span data-bind="text: name">
</span> $(<span data-bind="text:price"></span>)</label>
<input data-bind="attr: {name: id}, value: quantity"/>
<span data-bind="visible: subtotal" class="subtotal">
($<span data-bind="text: subtotal"></span>)
</span>
</div>
</div>
</div>
</script>
TheinnerspanelementusesatextdatabindingtodisplaythevalueofthesubtotalpropertyI
createdamomentago.Tomakethingsmoreinteresting,theouterspanelementusesanotherKO
binding;thisoneisvisible.Forthisbinding,thechildelementsarehiddenwhenthespecifiedproperty
isfalse-like(zero,null,undefined,orfalse).Fortruth-likevalues(1,true,oranon-nullobjectorarray),
thechildelementsaredisplayed.Ihavespecifiedthesubtotalvalueforthevisiblebinding,andthis
littletrickmeansthatIwilldisplayasubtotalonlywhentheuserentersanonzerovalueintotheinput
element.YoucanseetheresultinFigure3-4.
Figure3-4.Selectivelydisplayingsubtotals
65
www.it-ebooks.info
CHAPTER3ADDINGAVIEWMODEL
Youcanseehoweasyandquickitistocreatenewfeaturesoncethebasicstructurehasbeenadded
totheapplication.Somenewmarkupandalittlescriptgoalongway.And,asabonus,thesubtotal
featureworksseamlesslywiththespecialoffers;sincebothoperateontheviewmodel,thediscounts
appliedforthespecialoffersareseamlessly(andeffortlessly)incorporatedintothesubtotals.
AddingtheBasketLineItemsandTotal
Idon’twanttousetheinlinebasketapproachthatItookinChapter2becausesomeoftheproductsare
showntwiceandthedocumentistoolongtomaketheuserscrolldowntoseethetotalcostoftheir
selection.Instead,Iamgoingtocreateaseparatesetofbasketelementsthatwillbedisplayedalongside
theproducts.YoucanwhatIhavedoneinFigure3-5.
Figure3-5.Addingaseparatebasket
Listing3-9showsthechangesrequiredtosupportthebasket.
Listing3-9.AddingtheBasketElementsandLineItems
<!DOCTYPE html>
<html>
<head>
<title>CheeseLux</title>
<link rel="stylesheet" type="text/css" href="styles.css"/>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script src="jquery-ui-1.8.16.custom.js" type="text/javascript"></script>
<script src='knockout-2.0.0.js' type='text/javascript'></script>
66
www.it-ebooks.info
CHAPTER3ADDINGAVIEWMODEL
<link rel="stylesheet" type="text/css" href="jquery-ui-1.8.16.custom.css"/>
<noscript>
<meta http-equiv="refresh" content="0; noscript.html"/>
</noscript>
<script>
var cheeseModel = {
products: [
{category: "British Cheese", items : [
{id: "stilton", name: "Stilton", price: 9},
{id: "stinkingbishop", name: "Stinking Bishop", price: 17},
{id: "cheddar", name: "Cheddar", price: 17}]},
{category: "French Cheese", items: [
{id: "camembert", name: "Camembert", price: 18},
{id: "tomme", name: "Tomme de Savoie", price: 19},
{id: "morbier", name: "Morbier", price: 9}]},
{category: "Italian Cheese", items: [
{id: "gorgonzola", name: "Gorgonzola", price: 8},
{id: "fontina", name: "Fontina", price: 11},
{id: "parmesan", name: "Parmesan", price: 16}]}]
};
function mapProducts(func) {
$.each(cheeseModel.products, function(catIndex, outerItem) {
$.each(outerItem.items, function(itemIndex, innerItem) {
func(innerItem);
});
});
}
$(document).ready(function() {
$('#buttonDiv input:submit').button().css("font-family", "Yanone");
cheeseModel.specials = {
category: "Special Offers",
discount: 3,
ids: ["stilton", "tomme"],
items: []
};
mapProducts(function(item) {
if ($.inArray(item.id, cheeseModel.specials.ids) > -1) {
item.price -= cheeseModel.specials.discount;
cheeseModel.specials.items.push(item);
}
item.quantity = ko.observable(0);
item.subtotal = ko.computed(function() {
return this.quantity() * this.price;
}, item);
});
cheeseModel.total = ko.computed(function() {
var total = 0;
67
www.it-ebooks.info
CHAPTER3ADDINGAVIEWMODEL
});
mapProducts(function(elem) {
total += elem.subtotal();
});
return total;
ko.applyBindings(cheeseModel);
$('div.cheesegroup').not("#basket").css("width", "50%");
$('#basketTable a')
.button({icons: {primary: "ui-icon-closethick"}, text: false})
.click(function() {
var targetId = $(this).closest('tr').attr("data-prodId");
mapProducts(function(item) {
if (item.id == targetId) {
item.quantity(0);
}
});
})
});
</script>
<script id="categoryTmpl" type="text/html">
<div class="cheesegroup">
<div class="grouptitle" data-bind="text: category"></div>
<div data-bind="foreach: items">
<div class="groupcontent">
<label data-bind="attr: {for: id}" class="cheesename">
<span data-bind="text: name">
</span> $(<span data-bind="text:price"></span>)</label>
<input data-bind="attr: {name: id}, value: quantity"/>
<span data-bind="visible: subtotal" class="subtotal">
($<span data-bind="text: subtotal"></span>)
</span>
</div>
</div>
</div>
</script>
<script id="basketRowTmpl" type="text/html">
<tr data-bind="visible: quantity, attr: {'data-prodId': id}">
<td data-bind="text: name"></td>
<td>$<span data-bind="text: subtotal"></span></td>
<td><a href="#"></a></td>
</tr>
</script>
</head>
<body>
<div id="logobar">
<img src="cheeselux.png">
<span id="tagline">Gourmet European Cheese</span>
68
www.it-ebooks.info
CHAPTER3ADDINGAVIEWMODEL
</div>
<div id="basket" class="cheesegroup basket">
<div class="grouptitle">Basket</div>
<div class="groupcontent">
<table id="basketTable">
<thead><tr><th>Cheese</th><th>Subtotal</th><th></th></tr></thead>
<tbody data-bind="foreach: products">
<!-- ko template: {name: 'basketRowTmpl', foreach: items} -->
<!-- /ko -->
</tbody>
<tfoot>
<tr><td class="sumline" colspan=2></td></tr>
<tr>
<th>Total:</th><td>$<span data-bind="text: total"></span></td>
</tr>
</tfoot>
</table>
</div>
<div class="cornerplaceholder"></div>
<div id="buttonDiv">
<input type="submit" value="Submit Order"/>
</div>
</div>
<div data-bind="template: {name: 'categoryTmpl', data: specials}"></div>
<form action="/shipping" method="post">
<div data-bind="template: {name: 'categoryTmpl', foreach: products}"></div>
</form>
</body>
</html>
I’llstepthrougheachcategoryofchangethatImadeandexplaintheeffectithas.AsIdothis,please
reflectonhowlittlehastochangetoaddthisfeature.Onceagain,aviewmodelandsomebasic
applicationstructurecreateafoundationtowhichnewfeaturescanbequicklyandeasilyadded.
ExtendingtheViewModel
Thechangetotheviewmodelinthislistingistheadditionofthetotalproperty,whichisacomputed
observablethatsumstheindividualsubtotalvalues:
cheeseModel.total = ko.computed(function() {
var total = 0;
mapProducts(function(elem) {
total += elem.subtotal();
});
return total;
});
69
www.it-ebooks.info
CHAPTER3ADDINGAVIEWMODEL
AsImentionedpreviously,KOtracksdependenciesbetweenobservabledataitemsautomatically.
Anychangetoasubtotalvaluewillcausetotaltoberecalculatedandthenewvaluetobedisplayedin
elementsthatareboundtoit.
AddingtheBasketStructureandTemplate
TheouterstructureoftheHTMLelementsIaddedtothedocumentisjustaduplicateofacheese
categorytomaintainvisualconsistency.Theheartofthebasketisthetableelement,whichcontains
severaldatabindings:
<table id="basketTable">
<thead><tr><th>Cheese</th><th>Subtotal</th><th></th></tr></thead>
<tbody data-bind="foreach: products">
<!-- ko template: {name: 'basketRowTmpl', foreach: items} -->
<!-- /ko -->
</tbody>
<tfoot>
<tr><td class="sumline" colspan=2></td></tr>
<tr>
<th>Total:</th><td>$<span data-bind="text: total"></span></td>
</tr>
</tfoot>
</table>
ThemostimportantadditionhereistheoddlyformattedHTMLcomments.Thisisknownasa
containerlessbinding,anditallowsmetoapplythetemplate bindingwithoutneedingacontainer
elementforthecontentthatwillbeduplicated.Addingrowstoatablefromanestedarrayisaperfect
situationforthistechniquebecauseaddinganelementjustsoIcanapplythebindingwouldcause
layoutproblems.Thecontainerlessbindingiscontainedwithinaregularforeachbinding,butyoucan
nestthebindingcommentsmuchasyouwouldregularelements.
Theotherbindingisasimpletextvaluebinding,whichdisplaystheoveralltotalforthebasket,
usingthecalculatedtotalobservableIcreatedamomentago.Idon’thavetotakeanyactiontomake
surethatthetotalisup-to-date;KOmanagesthechainofdependenciesbetweenthetotal,subtotal,
andquantitypropertiesintheviewmodel.
ThetemplatethatIaddedtoproducethetablerowshasfourdatabindings:
<script id="basketRowTmpl" type="text/html">
<tr data-bind="visible: quantity, attr: {'data-prodId': id}">
<td data-bind="text: name"></td>
<td>$<span data-bind="text: subtotal"></span></td>
<td><a href="#"></a></td>
</tr>
</script>
Youhaveseenthesetypesofbindingpreviously.Thevisiblebindingonthetrelementensures
thattablerowsarevisibleonlyforthosecheesesforwhichthequantityisn’tzero;thispreventsthe
basketfrombeingfilledupwithrowsforproductsthattheuserisn’tinterestedin.
Notetheattrbindingonthetrelement.IhavedefinedacustomattributeusingtheHTML5data
attributefeaturethatembedstheidvalueoftheproductthattherowrepresentsintothetrelement.I’ll
explainwhyIdidthisshortly.
70
www.it-ebooks.info
CHAPTER3ADDINGAVIEWMODEL
Ialsomovedthesubmitbuttonsothatitisunderthebasket,makingiteasierfortheusertosubmit
theirorder.ThestylethatIassignedtothebasketelementsusesthefixedvaluefortheCSSposition
property,meaningthatthebasketwillalwaysbevisible,evenastheuserscrollsdownthepage.To
accommodatethebasket,IusedjQuerytoapplyanewvaluefortheCSSwidthpropertydirectlytothe
cheesecategoryelements(butnotthebasketitself):
$('div.cheesegroup').not("#basket").css("width", "50%");
RemovingItemsfromtheBasket
ThelastsetofchangesbuildsontheaelementsthatareaddedtoeachtablerowinthebasketRowTmpl
template:
$('#basketTable a')
.button({icons: {primary: "ui-icon-closethick"}, text: false})
.click(function() {
var targetId = $(this).closest('tr').attr("data-prodId");
mapProducts(function(item) {
if (item.id == targetId) {
item.quantity(0);
}
});
})
IusejQuerytoselectalltheaelementsandusejQueryUItocreatebuttonsfromthem.jQueryUI
themesincludeasetoficons,andtheobjectthatIpasstothejQueryUIbuttonmethodcreatesabutton
thatusesoneoftheseimagesanddisplaysnotext.Thisgivesmeanicesmallbuttonwithacross.
Intheclickfunction,IusejQuerytonavigatefromtheaelementthattriggeredtheclickeventto
thefirstancestortrelementusingtheclosestmethod.Thisselectsthetrelementthatcontainsthe
customdataattributeIinsertedinthetemplateearlierandthatIreadusingtheattrmethod:
var targetId = $(this).closest('tr').attr("data-prodId");
Thisstatementletsmedeterminetheidoftheproducttheuserwantstoremovefromthebasket.I
thenusethemapProductsfunctiontofindthematchingcheeseobjectandsetthequantitytozero.Since
quantityisanobservabledataitem,KOdisseminatesthenewvalue,whichcausesthesubtotalvalueto
berecalculatedandthevisiblebindingonthecorrespondingtrelementtobereevaluated.Sincethe
quantityiszero,thetablerowwillbehiddenautomatically.And,sincesubtotalisobservable,thetotal
willalsoberecalculated,andthenewvalueisdisplayedtotheuser.Asyoucansee,itisusefultohavea
viewmodelwherethedependenciesbetweendatavaluesaremanagedseamlessly.Thenetresultisa
dynamicbasketthatisalwaysconsistentwiththevaluesintheviewmodelandsoalwayspresentsthe
correctinformationtotheuser.
FinishingtheExample
BeforeIfinishthistopic,Ijustwanttotweakacoupleofthings.First,thebasketlooksprettypoorwhen
noitemshavebeenselectedbytheuser,asshowninFigure3-6.Toaddressthis,Iwilldisplaysome
placeholdertextwhenthebasketisempty.
71
www.it-ebooks.info
CHAPTER3ADDINGAVIEWMODEL
Figure3-6.Theemptybasket
Second,theuserhasnowaytoclearthebasketwithasingleaction,soIwilladdabuttonthatwill
resetthequantitiesofalloftheproductstozero.Finally,bymovingthesubmitbuttonoutsidetheform
element,Ihavelosttheabilitytorelyonthedefaultaction.Imustaddaneventhandlersothattheuser
cansubmittheform.Listing3-10showstheHTMLelementsthatIhaveaddedtosupportthesefeatures.
Listing3-10.AddingElementstoFinishtheExample
...
<body>
<div id="logobar">
<img src="cheeselux.png">
<span id="tagline">Gourmet European Cheese</span>
</div>
<div id="basket" class="cheesegroup basket">
<div class="grouptitle">Basket</div>
<div class="groupcontent">
<div class="description" data-bind="ifnot: total">
No products selected
</div>
<table id="basketTable" data-bind="visible: total">
<thead>
<tr><th>Cheese</th><th>Subtotal</th><th></th></tr>
</thead>
<tbody data-bind="template: {name:'basketRowTmpl', foreach: items}">
</tbody>
<tfoot>
<tr><td class="sumline" colspan=2></td></tr>
<tr>
<th>Total:</th><td>$<span data-bind="text: total"></span></td>
</tr>
</tfoot>
</table>
72
www.it-ebooks.info
CHAPTER3ADDINGAVIEWMODEL
</div>
<div class="cornerplaceholder"></div>
<div id="buttonDiv">
<input type="submit" value="Submit Order"/>
<input type="reset" value="Reset"/>
</div>
</div>
<div data-bind="template: {name: 'categoryTmpl', data: specials}"></div>
<form action="/shipping" method="post">
<div data-bind="template: {name: 'categoryTmpl', foreach: products}"></div>
</form>
</body>
...
Ihaveusedtheifnotbindingonthedivelementthatcontainstheplaceholdertext.KOdefinesa
pairofbindings,ifandifnot,thataresimilartothevisiblebindingbutthataddandremoveelements
totheDOM,ratherthansimplyhidingthemfromview.Theifbindingshowsitselementswhenthe
specifiedviewmodelpropertyistrue-likeandhidesthemifitisfalse-like.Theifnotbindingisinverted;
itshowsitselementswhenthepropertyistrue-like.
Byspecifyingtheifnotbindingwiththetotalproperty,Iensurethatmyplaceholderelementis
shownonlywhentotaliszero,whichhappenswhenallofthesubtotalvaluesarezero,whichhappens
whenallofthequantityvaluesarezero.Onceagain,IamrelyingonKO’sabilitytomanagethe
dependenciesbetweenobservabledataitemstogettheeffectIrequire.
Iwantthetableelementtobeinvisiblewhentheplaceholderisshowing,soIhaveusedthevisible
binding.
Icouldhaveusedtheifbinding,butdoingsowouldhavecausedaproblem.Thebindingtothe
totalpropertymeansthatthetablewillnotbeshowninitially,andwiththeifbinding,theelement
wouldhavebeenremovedfromtheDOM.Thismeansthattheaelementswouldalsonotbepresent
whenItrytoselectthemtosetuptheremovebuttons.Thevisiblebindingleavestheelementsinthe
documentforjQuerytofindbuthidesthemfromtheuser.
YoumightwonderwhyIdon’tmovethejQueryselectionsothatitisperformedbeforethecallto
ko.applyBindings.ThereasonisthattheaelementsIwanttoselectwithjQueryarecontainedintheKO
template,whichisn’tusedtocreateelementsuntiltheapplyBindingsmethodiscalled.Thereisnogood
wayaroundthis,andsothevisiblebindingisrequired.
TheonlyotherchangetotheHTMLelementsistheadditionofaninputelementwhosetypeis
reset.Thiselementisoutsideoftheformelement,soIwillhavetohandletheclickeventtoremove
itemsfromthebasket.Listing3-11showsthecorrespondingchangestothescriptelement.
Listing3-11.EnhancingtheScripttoFinishtheExample
...
<script>
// ...code removed for brevity... //
$(document).ready(function() {
$('#buttonDiv input').button().css("font-family", "Yanone")
.click(function() {
73
www.it-ebooks.info
CHAPTER3ADDINGAVIEWMODEL
});
if (this.type == "submit") {
$('form').submit();
} else if (this.type == "reset") {
mapProducts(function(item) {
item.quantity(0);
})
}
// ...code removed for brevity... //
});
</script>
Ihaveshownonlypartofthescriptinthelistingbecausethechangesarequiteminor.NoticehowI
amabletousejQueryandplainJavaScripttomanipulatetheviewmodel.Idon’tneedtoaddanycode
forthebasketplaceholder,sinceitwillbemanagedbyKO.Infact,allIneeddoiswidenthejQuery
selectionsothatIcreatejQueryUIbuttonwidgetsforboththesubmitandresetinputelementsandadd
aclickhandlerfunction.InthefunctionIsubmittheformorchangethequantityvaluestozero
dependingonwhichbuttontheuserclicks.YoucanseetheplaceholderforthebasketinFigure3-7.
Figure3-7.Usingaplaceholderwhenthebasketisempty
Youwillhavetoloadtheexamplesinabrowserifyouwanttoseehowthebuttonswork.Theeasiest
waytodothisistousethesourcecodedownloadthataccompaniesthisbookandthatisavailable
withoutchargeatApress.com.
Summary
Inthischapter,Ishowedyouhowtoembracethekindofdesignphilosophythatyoumayhave
previouslyusedindesktoporserver-sidedevelopment,oratleastasmuchofthatphilosophyasmakes
senseforyourproject.
Byaddingaviewmodeltomywebapp,Iwasabletocreateamuchmoredynamicversionofthe
exampleapplication;it’sonethatismorescalable,easiertotestandmaintain,andmakeschangesand
enhancementabreeze.
Youmayhavenoticedthattheshapeofastructuredwebapplicationchangessothatthereisalot
morecoderelativetotheamountofHTMLmarkup.Thisisagoodthing,becauseitputsthecomplexity
74
www.it-ebooks.info
CHAPTER3ADDINGAVIEWMODEL
oftheapplicationwhereyoucanbetterunderstand,test,andmodifyit.TheHTMLbecomesaseriesof
viewsortemplatesforyourdata,drivenfromtheviewmodelviathestructurelibrary.Icannot
emphasizethebenefitsofembracingthisapproachenough;itreallydoessetthefoundationfor
professional-levelwebappsandwillmakecreating,enhancing,andmaintainingyourprojectssimpler,
easier,andmoreenjoyable.
75
www.it-ebooks.info
CHAPTER 4
Using URL Routing
Inthischapter,Iwillshowyouhowtoaddanotherserver-sideconcepttoyourwebapp:URLrouting.
TheideabehindURLroutingisverysimple:weassociateJavaScriptfunctionswithinternalURLs.An
internalURLisonethatisrelativetothecurrentdocumentandcontainsahashfragment.Infact,they
areusuallyexpressedasjustthehashfragmentonitsown,suchas#summary.
Undernormalcircumstances,whentheuserclicksalinkthatpointstoaninternalURL,thebrowser
willseewhetherthereisanelementinthedocumentthathasanidattributevaluethatmatchesthe
fragmentand,ifthereis,scrolltomakethatelementvisible.
WhenweuseURLrouting,werespondtothesenavigationchangesbyexecutingJavaScript
functions.Thesefunctionscanshowandhideelements,changetheviewmodel,orperformothertasks
youmightneedinyourapplication.Usingthisapproach,wecanprovidetheuserwithamechanismto
navigatethroughourapplication.
Wecould,ofcourse,useevents.Theproblemis,onceagain,scale.Handlingeventstriggeredby
elementsisaperfectlyworkableandacceptableapproachforsmallandsimplewebapplications.For
largerandmorecomplexapps,weneedsomethingbetter,andURLroutingprovidesaniceapproach
thatissimple,iselegant,andscaleswell.Addingnewfunctionalareastothewebapp,andproviding
userswiththemeanstousethem,becomesincrediblysimpleandrobustwhenweuseURLsasthe
navigationmechanism.
BuildingaSimpleRoutedWebApplication
ThebestwaytoexplainURLroutingiswithasimpleexample.Listing4-1showsabasicwebapplication
thatreliesonrouting.
Listing4-1.ASimpleRoutedWebApplication
<!DOCTYPE html>
<html>
<head>
<title>Routing Example</title>
<link rel="stylesheet" type="text/css" href="jquery-ui-1.8.16.custom.css"/>
<link rel="stylesheet" type="text/css" href="styles.css"/>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script src="jquery-ui-1.8.16.custom.js" type="text/javascript"></script>
<script src='knockout-2.0.0.js' type='text/javascript'></script>
<script src='utils.js' type='text/javascript'></script>
<script src='signals.js' type='text/javascript'></script>
<script src='crossroads.js' type='text/javascript'></script>
<script src='hasher.js' type='text/javascript'></script>
77
www.it-ebooks.info
CHAPTER4USINGURLROUTING
<script>
var viewModel = {
items: ["Apple", "Orange", "Banana"],
selectedItem: ko.observable("Apple")
};
$(document).ready(function() {
ko.applyBindings(viewModel);
$('div.catSelectors').buttonset();
hasher.initialized.add(crossroads.parse, crossroads);
hasher.changed.add(crossroads.parse, crossroads);
hasher.init();
crossroads.addRoute("select/Apple", function() {
viewModel.selectedItem("Apple");
});
crossroads.addRoute("select/Orange", function() {
viewModel.selectedItem("Orange");
});
crossroads.addRoute("select/Banana", function() {
viewModel.selectedItem("Banana");
});
});
</script>
</head>
<body>
<div class="catSelectors" data-bind="foreach: items">
<a data-bind="formatAttr: {attr: 'href', prefix: '#select/', value: $data},
css: {selectedItem: ($data == viewModel.selectedItem())}">
<span data-bind="text: $data"></span>
</a>
</div>
<div data-bind="foreach: items">
<div class="item" data-bind="fadeVisible: $data == viewModel.selectedItem()">
The selected item is: <span data-bind="text: $data"></span>
</div>
</div>
</body>
</html>
Thisisarelativelyshortlisting,butthereisalotgoingon,soI’llbreakthingsdownandexplainthe
movingpartsinthesectionsthatfollow.
AddingtheRoutingLibrary
Onceagain,IamgoingtouseapublicallyavailablelibrarytogettheeffectIrequire.ThereareafewURL
routinglibrariesaround,buttheonethatIlikeiscalledCrossroads.Itissimple,reliable,andeasytouse.
Ithasonedrawback,whichisthatitdependsontwootherlibrariesbythesameauthor.Iliketosee
dependenciesrolledintoasinglelibrary,butthisisnotauniversallyheldpreference,anditjustmeans
thatwehavetodownloadacoupleofextrafiles.Table4-1liststheprojectsandtheJavaScriptfilesthat
78
www.it-ebooks.info
CHAPTER4USINGURLROUTING
werequirefromthedownloadarchives,whichshouldbecopiedintotheNode.jsservercontent
directory.(Allthreefilesarepartofthesourcecodedownloadforthisbookifyoudon’twantto
downloadthesefilesindividually.ThedownloadisfreelyavailableatApress.com.)
Table4-1.CrossroadsJavaScriptLibraries
Library Name
URL
Required File
Crossroads
http://millermedeiros.github.com/crossroads.js/ crossroads.js
Signals
http://millermedeiros.github.com/js-signals/ signals.js
Hasher
https://github.com/millermedeiros/hasher/ hasher.js
IaddedCrossroads,itssupportinglibraries,andmynewcheeseutils.jsfileintotheHTML
documentusingscriptelements:
...
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script src="jquery-ui-1.8.16.custom.js" type="text/javascript"></script>
<script src='knockout-2.0.0.js' type='text/javascript'></script>
<script src='utils.js' type='text/javascript'></script>
<script src='signals.js' type='text/javascript'></script>
<script src='crossroads.js' type='text/javascript'></script>
<script src='hasher.js' type='text/javascript'></script>
<script>
...
AddingtheViewModelandContentMarkup
URLroutingworksextremelywellwhencombinedwithaviewmodelinawebapplication.Forthis
initialapplication,Ihavecreatedaverysimpleviewmodel,asfollows:
var viewModel = {
items: ["Apple", "Orange", "Banana"],
selectedItem: ko.observable("Apple")
};
Therearetwopropertiesintheviewmodel.Theitemspropertyreferstoanarrayofthreestrings.
TheselectedItempropertyisanobservabledataitemthatkeepstrackofwhichitemispresently
selected.Iusethesevalueswithdatabindingstogeneratethecontentinthedocument,likethis:
...
<div data-bind="foreach: items">
<div class="item" data-bind="fadeVisible: $data == viewModel.selectedItem()">
The selected item is: <span data-bind="text: $data"></span>
</div>
</div>
...
ThebindingsthatKOsupportsbydefaultareprettybasic,butitiseasytocreatecustomones,which
isexactlywhatIhavedoneforthefadeVisiblebindingreferredtointhelisting.Listing4-2showsthe
79
www.it-ebooks.info
CHAPTER4USINGURLROUTING
definitionofthisbinding,whichIhaveplacedinafilecalledutils.js(whichyoucanseeimportedina
scriptelementinListing4-1).Thereisnorequirementtouseanexternalfile;IhaveusedonebecauseI
intendtoemploythisbindingagainwhenIaddroutingtotheCheeseLuxexamplelaterinthechapter.
Listing4-2.DefiningaCustomBinding
ko.bindingHandlers.fadeVisible = {
init: function(element, accessor) {
$(element)[accessor() ? "show" : "hide"]();
},
}
update: function(element, accessor) {
if (accessor() && $(element).is(":hidden")) {
var siblings = $(element).siblings(":visible");
if (siblings.length) {
siblings.fadeOut("fast", function() {
$(element).fadeIn("fast");
})
} else {
$(element).fadeIn("fast");
}
}
}
Creatingacustombindingisassimpleasaddinganewpropertytotheko.bindinghandlersobject;
thenameofthepropertywillbethenameofthenewbinding.Thevalueofthepropertyisanobjectwith
twomethods:initandupdate.Theinitmethodiscalledwhenko.applyBindingsiscalled,andthe
updatemethodiscalledwhenobservabledataitemsthatthebindingdependsonchange.
Theargumentstobothmethodsaretheelementtowhichthebindinghasbeenappliedtoandan
accessorobjectthatprovidesaccesstothebindingargument.Thebindingargumentiswhateverfollows
thebindingname:
data-bind="fadeVisible: $data == viewModel.selectedItem()"
Ihaveused$datainmybindingargument.Whenusingaforeachbinding,$datareferstothe
currentiteminthearray.IcheckthisvalueagainsttheselectedItemobservabledataitemintheview
model.Ihavetorefertotheobservablethroughtheglobalvariablebecauseitisnotwithinthecontextof
theforeachbinding,andthismeansIneedtotreattheobservablelikeafunctiontogetthevalue.When
KOcallstheinitorupdatemethodofmycustombinding,theexpressioninthebindingargumentis
resolved,andtheresultofcallingaccessor()istrue.
Inmycustombinding,theinitmethodusesjQuerytoshoworhidetheelementtowhichthe
bindinghasbeenappliedbasedontheaccessorvalue.Thismeansthatonlytheelementsthat
correspondtotheselectedItemobservablearedisplayed.
Theupdatemethodworksdifferently.IusejQueryeffectstoanimatethetransitionfromonesetof
elementstoanother.Iftheupdatemethodisbeingcalledfortheelementsthatshouldbedisplayed,I
selecttheelementsthatarepresentlyvisibleandcallthefadeOutmethod.Thiscausestheelementsto
graduallybecometransparentandtheninvisible;oncethishashappened,IthenusefadeIntomakethe
requiredelementsvisible.Theresultisasmoothtransitionfromonesetofelementstoanother.
80
www.it-ebooks.info
CHAPTER4USINGURLROUTING
AddingtheNavigationMarkup
Igenerateasetofaelementstoprovidetheuserwiththemeanstoselectdifferentitems;inmysimple
application,theseformthenavigationmarkup.Hereisthemarkup:
<div class="catSelectors" data-bind="foreach: items">
<a data-bind="formatAttr: {attr: 'href', prefix: '#select/', value: $data},
css: {selectedItem: ($data == viewModel.selectedItem())}">
<span data-bind="text: $data">
</a>
</div>
AsImentionedinChapter3,thebuilt-inKObindingssimplyinsertvaluesintothemarkup.Mostof
thetime,thiscanbeworkedaroundbyaddingspanordivelementstoprovidestructuretowhich
bindingscanbeattached.Thisapproachdoesn’tworkwhenitcomestoattributevalues,whichisa
problemwhenusingURLrouting.WhatIwantisaseriesofaelementswhosehrefattributecontainsa
valuefromtheviewmodel,likethis:
<a href="#/select/Apple">Apple</a>
Ican’tgettheresultIwantfromthestandardattrbinding,soIhavecreatedanothercustomone.
Listing4-3showsthedefinitionoftheformatAttrbinding.I’llbeusingthisbindinglater,soIhave
defineditintheutil.jsfile,alongsidethefadeVisiblebinding.
Listing4-3.DefiningtheformatAttrCustomBinding
function composeString(bindingConfig ) {
var result = bindingConfig.value;
if (bindingConfig.prefix) { result = bindingConfig.prefix + result; }
if (bindingConfig.suffix) { result += bindingConfig.suffix;}
return result;
}
ko.bindingHandlers.formatAttr = {
init: function(element, accessor) {
$(element).attr(accessor().attr, composeString(accessor()));
},
update: function(element, accessor) {
$(element).attr(accessor().attr, composeString(accessor()));
}
}
Thefunctionalityofthisbindingcomesthroughtheaccessor.ThebindingargumentIhaveusedon
theelementisaJavaScriptobject,whichbecomesobviouswithsomejudiciousreformatting:
formatAttr:
{attr: 'href',
prefix: '#select/',
value: $data
},
css: {selectedItem: ($data == viewModel.selectedItem())}
KOresolvesthedatavaluesbeforepassingthisobjecttomyinitorupdatemethods,givingme
somethinglikethis:
81
www.it-ebooks.info
CHAPTER4USINGURLROUTING
{attr: 'href',
prefix: '#select/',
value: Apple}
Iusethepropertiesofthisobjecttocreatetheformattedstring(usingthecomposeStringfunctionI
definedalongsidethecustombinding)tocombinethecontentofvaluepropertywiththevalueofthe
prefixandsuffixpropertiesiftheyaredefined.
Therearetwootherbindings.ThecssbindingappliesandremovesaCSSclass;Iusethisbindingto
applytheselectedItemclass.Thiscreatesasimpletogglebutton,showingtheuserwhichbuttonis
clicked.Thetextbindingisappliedtoachildspanelement.Thisistoworkaroundaproblemwhere
jQueryUIandKObothassumecontroloverthecontentsoftheaelement;applyingthetextattributeto
anestedelementavoidsthisconflict.IneedthisworkaroundbecauseIusejQueryUItocreatebutton
widgetsfromthenavigationelements,likethis:
<script>
var viewModel = {
items: ["Apple", "Orange", "Banana"],
selectedItem: ko.observable("Apple")
};
$(document).ready(function() {
ko.applyBindings(viewModel);
$('div.catSelectors').buttonset();
... other statements removed for brevity...
});
</script>
Byapplyingthebuttonsetmethodtoacontainerelement,Iamabletocreateasetofbuttonsfrom
thechildaelements.Ihaveusedbuttonset,ratherthanbutton,sothatjQueryUIwillstyletheelements
inacontiguousblock.YoucanseetheeffectthatthiscreatesinFigure4-1.
Figure4-1.Thebasicapplicationtowhichroutingisapplied
Thereisnospacebetweenbuttonscreatedbythebuttonsetmethod,andtheouteredgesoftheset
arenicelyrounded.Youcanalsoseeoneofthecontentelementsinthefigure.Theideaisthatclicking
oneofthebuttonswillallowtheusertodisplaythecorrespondingcontentitem.
82
www.it-ebooks.info
f
CHAPTER4USINGURLROUTING
ApplyingURLRouting
Ihavealmosteverythinginplace:asetofnavigationalcontrolsandasetofcontentelements.Inow
needtotiethemtogether,whichIdobyapplyingtheURLrouting:
<script>
var viewModel = {
items: ["Apple", "Orange", "Banana"],
selectedItem: ko.observable("Apple")
};
$(document).ready(function() {
ko.applyBindings(viewModel);
$('div.catSelectors').buttonset();
hasher.initialized.add(crossroads.parse, crossroads);
hasher.changed.add(crossroads.parse, crossroads);
hasher.init();
crossroads.addRoute("select/Apple", function() {
viewModel.selectedItem("Apple");
});
crossroads.addRoute("select/Orange", function() {
viewModel.selectedItem("Orange");
});
crossroads.addRoute("select/Banana", function() {
viewModel.selectedItem("Banana");
});
});
</script>
ThefirstthreeofthehighlightedstatementssetuptheHasherlibrarysothatitworkswith
Crossroads.HasherrespondstotheinternalURLchangethroughthelocation.hashbrowserobjectand
notifiesCrossroadswhenthereisachange.
CrossroadsexaminesthenewURLandcomparesittoeachoftheroutesithasbeengiven.Routes
aredefinedusingtheaddRoutemethod.ThefirstargumenttothismethodistheURLweareinterested
in,andthesecondargumentisafunctiontoexecuteiftheuserhasnavigatedtothatURL.So,for
example,iftheusernavigatesto#select/Apple,thenthefunctionthatsetstheselectedItemobservable
intheviewmodeltoApplewillbeexecuted.
TipWedon’thavetospecifythe#characterwhenusingtheaddRoutemethodbecauseHasherremovesit
beforenotifyingCrossroadsofachange.
Intheexample,Ihavedefinedthreeroutes,eachofwhichcorrespondstooneoftheURLsthatI
createdusingtheformatAttrbindingontheaelements.
83
www.it-ebooks.info
CHAPTER4USINGURLROUTING
ThisisattheheartofURLrouting.YoucreateasetofURLroutesthatdrivethebehavioroftheweb
appandthencreateelementsinthedocumentthatnavigatetothoseURLs.Figure4-2showstheeffect
ofsuchnavigationintheexample.
Figure4-2.Navigatingthroughtheexamplewebapp
Whentheuserclicksabutton,thebrowsernavigatestotheURLspecifiedbythehrefattributeof
theunderlyingaelement.Thisnavigationchangeisdetectedbytheroutingsystem,whichtriggersthe
functionthatcorrespondstotheURL.Thefunctionchangesthevalueofanobservableitemintheview
model,andthatcausestheelementsthatrepresenttheselecteditemtobedisplayedbytheuser.
Theimportantpointtounderstandisthatweareworkingwiththebrowser’snavigation
mechanism.Whentheuserclicksoneofthenavigationelements,thebrowsermovestothetargetURL;
althoughtheURLiswithinthesamedocument,thebrowser’shistoryandURLbarareupdated,asyou
canseeinthefigure.
Thisconferstwobenefitsonawebapplication.ThefirstisthattheBackbuttonworksthewaythat
mostusersexpectittowork.ThesecondisthattheusercanenteraURLmanuallyandnavigatetoa
specificpartoftheapplication.Toseebothofthesebehaviorsinaction,followthesesteps:
1.
Loadthelistinginthebrowser.
2.
ClicktheOrangebutton.
3.
Entercheeselux.com/#select/Bananaintothebrowser’sURLbar.
4.
Clickthebrowser’sBackbutton.
WhenyouclickedtheOrangebutton,theOrangeitemwasselected,andthebuttonwashighlighted.
SomethingsimilarhappensfortheBananaitemwhenyouenteredtheURL.Thisisbecausethe
navigationmechanismfortheapplicationisnowmediatedbythebrowser,andthisishowweareable
touseURLroutingtodecoupleanotheraspectoftheapplication.
Thefirstbenefitis,tomymind,themostuseful.WhentheuserclickstheBackbutton,thebrowser
navigatesbacktothelastvisitedURL.Thisisanavigationchange,andifthepreviousURLiswithinour
document,thenewURLismatchedagainstthesetofroutesdefinedbytheapplication.Thisisan
opportunitytounwindtheapplicationstatetothepreviousstep,whichinthecaseofthesample
applicationdisplaystheOrangebutton.Thisisamuchmorenaturalwayofworkingforauser,especially
comparedtousingregularevents,whereclickingtheBackbuttontendstonavigatetothesitetheuser
visitedbeforeourapplication.
84
www.it-ebooks.info
CHAPTER4USINGURLROUTING
ConsolidatingRoutes
Inthepreviousexample,Idefinedeachrouteandthefunctionitexecutedseparately.Ifthiswerethe
onlywaytodefineroutes,acomplexwebappwouldendupwithamorassofroutesandfunctions,and
therewouldbenoadvantageoverregulareventhandling.Fortunately,URLsroutingisveryflexible,and
wecanconsolidateourrouteswithease.Idescribethetechniquesavailableforthisinthesectionsthat
follow.
UsingVariableSegments
Listing4-4showshoweasyitistoconsolidatethethreeroutesfromtheearlierdemonstrationintoa
singleroute.
Listing4-4.ConsolidatingRoutes
<script>
var viewModel = {
items: ["Apple", "Orange", "Banana"],
selectedItem: ko.observable("Apple")
};
$(document).ready(function() {
ko.applyBindings(viewModel);
$('div.catSelectors').buttonset();
hasher.initialized.add(crossroads.parse, crossroads);
hasher.changed.add(crossroads.parse, crossroads);
hasher.init();
crossroads.addRoute("select/{item}", function(item) {
viewModel.selectedItem(item);
});
});
</script>
ThepathsectionofaURLismadeupofsegments.Forexample,theURLpathselect/Applehastwo
segments,whichareselectandApple.WhenIspecifyaroute,likethis:
/select/Apple
theroutewillmatchaURLonlyifbothsegmentsmatchexactly.Inthelisting,Ihavebeenableto
consolidatemyroutesbyaddingavariablesegment.AvariablesegmentallowsaroutetomatchaURL
thathasanyvalueforthecorrespondingsegment.So,tobeclear,allofthenavigationURLsinthe
simplewebappwillmatchmynewroute:
select/Apple
select/Orange
select/Banana
Thefirstsegmentisstillstatic,meaningthatonlyURLswhosefirstsegmentisselectwillmatch,but
Ihaveessentiallyaddedawildcardforthesecondsegment.
85
www.it-ebooks.info
CHAPTER4USINGURLROUTING
SothatIcanrespondappropriatelytotheURL,thecontentofthevariablesegmentispassedtomy
functionasanargument.IusethisargumenttochangethevalueoftheselectedItemobservableinthe
viewmodel,meaningthataURLof/select/Appleresultsinacalllikethis:
viewModel.selectedItem('Apple');
andaURLofselect/Cherrywillresultinacalllikethis:
viewModel.selectedItem('Cherry');
DealingwithUnexpectedSegmentValues
ThatlastURLisaproblem.Thereisn’tanitemcalledCherryinmywebapp,andsettingtheviewmodel
observabletothisvaluewillcreateanoddeffectfortheuser,asshowninFigure4-3.
Figure4-3.Theresultofanunexpectedvariablesegmentvalue
TheflexibilitythatcomeswithURLroutingcanalsobeaproblem.Beingabletonavigatetoa
specificpartoftheapplicationisausefultoolfortheuser,but,aswithallopportunitiesfortheuserto
provideinput,wehavetoguardagainstunexpectedvalues.Formyexampleapplication,thesimplest
waytovalidatevariablesegmentvaluesistocheckthecontentsofthearrayintheviewmodel,asshown
inListing4-5.
Listing4-5.IgnoringUnexpectedSegmentValues
...
crossroads.addRoute("select/{item}", function(item) {
if (viewModel.items.indexOf(item) > -1) {
viewModel.selectedItem(item);
}
});
...
Inthislisting,Ihavetakenthepathofleastresistance,whichistosimplyignoreunexpectedvalues.
Therearelotsofalternativeapproaches.Icouldhavedisplayedanerrormessageor,asListing4-6
shows,embracedtheunexpectedvalueandaddedittotheviewmodel.
86
www.it-ebooks.info
CHAPTER4USINGURLROUTING
Listing4-6.DealingwithUnexpectedValuesbyAddingThemtotheViewModel
<script>
var viewModel = {
items: ko.observableArray(["Apple", "Orange", "Banana"]),
selectedItem: ko.observable("Apple")
};
$(document).ready(function() {
ko.applyBindings(viewModel);
$('div.catSelectors').buttonset();
hasher.initialized.add(crossroads.parse, crossroads);
hasher.changed.add(crossroads.parse, crossroads);
hasher.init();
crossroads.addRoute("select/{item}", function(item) {
if (viewModel.items.indexOf(item)== -1) {
viewModel.items.push(item);
$('div.catSelectors').buttonset();
}
viewModel.selectedItem(item);
});
});
</script>
Ifthevalueofthevariablesegmentisn’toneofthevaluesintheitemsarrayintheviewmodel,then
Iusethepushmethodtoaddthenewvalue.Ichangedtheviewmodelsothattheitemsarrayisan
observableitemusingtheko.observableArraymethod.Anobservablearrayislikearegularobservable
dataitem,exceptthatbindingssuchasforeachareupdatedwhenthecontentofthearraychanges.
UsinganobservablearraymeansthataddinganitemcausesKnockouttogeneratecontentand
navigationelementsinthedocument.
ThelaststepinthisprocessistocallthejQueryUIbuttonsetmethodagain.KOhasnoknowledge
ofthejQueryUIstylesthatareappliedtoanaelementtocreateabutton,andthismethodhastobe
reappliedtogettherighteffect.Youcanseetheresultofnavigatingto#select/CherryinFigure4-4.
Figure4-4.Incorporatingunexpectedsegmentvaluesintotheapplicationstate
87
www.it-ebooks.info
CHAPTER4USINGURLROUTING
UsingOptionalSegments
ThelimitationofvariablesegmentsisthattheURLmustcontainasegmentvaluetomatcharoute.For
example,therouteselect/{item}willmatchanytwo-segmentURLwherethefirstsegmentisselect,
butitwon’tmatchselect/Apple/Red(becausetherearetoomanysegments)orselect(becausethere
aretoofewsegments).
Wecanuseoptionalsegmentstoincreasetheflexibilityofourroutes.Listing4-7showsthe
applicationonanoptionalsegmenttotheexample.
Listing4-7.UsinganOptionalSegmentinaRoute
...
crossroads.addRoute("select/:item:", function(item) {
if (!item) {
item = "Apple";
} else if (viewModel.items.indexOf(item)== -1) {
viewModel.items.push(item);
$('div.catSelectors').buttonset();
}
viewModel.selectedItem(item);
});
...
Tocreateanoptionalsegment,Isimplyreplacethebracecharacterswithcolonssothat{item}
becomes:item:.Withthischange,theroutewillmatchURLsthathaveoneortwosegmentsandwhere
thefirstsegmentisselect.Ifthereisnosecondsegment,thentheargumentpassedtothefunctionwill
benull.Inmylisting,IdefaulttotheApplevalueifthisisthecase.Aroutecancontainasmanystatic,
variable,andoptionalsegmentsasyourequire.Iwillkeepmyroutessimpleinthisexample,butyoucan
createprettymuchanycombinationyourequire.
AddingaDefaultRoute
Withtheintroductionoftheoptionalsegment,myroutewillmatchone-andtwo-segmentURLs.The
finalrouteIwanttoaddisadefaultroute,whichisonethatwillbeinvokedwhentherearenosegments
intheURLatall.ThisisrequiredtocompletethesupportfortheBackbutton.ToseetheproblemIam
solving,loadthelistingintothebrowser,clickoneofthenavigationelements,andthenhittheBack
button.Youcanseetheeffect—or,rather,thelackofaneffect—inFigure4-5.
Figure4-5.Navigatingbacktotheapplicationstartingpoint
88
www.it-ebooks.info
CHAPTER4USINGURLROUTING
Theapplicationdoesn’tresettoitsoriginalstatewhentheBackbuttonisclicked.Thishappensonly
whenclickingtheBackbuttontakesthebrowserbacktothebaseURLforthewebapp(whichis
http://cheeselux.cominmycase).NothinghappensbecausethebaseURLdoesn’tmatchtheroutes
thattheapplicationdefines.Listing4-8showstheadditionofanewroutetofixthisproblem.
Listing4-8.AddingaRoutefortheBaseURL
...
<script>
var viewModel = {
items: ko.observableArray(["Apple", "Orange", "Banana"]),
selectedItem: ko.observable("Apple")
};
$(document).ready(function() {
ko.applyBindings(viewModel);
$('div.catSelectors').buttonset();
hasher.initialized.add(crossroads.parse, crossroads);
hasher.changed.add(crossroads.parse, crossroads);
hasher.init();
crossroads.addRoute("select/:item:", function(item) {
if (!item) {
item = "Apple";
} else if (viewModel.items.indexOf(item)== -1) {
viewModel.items.push(item);
$('div.catSelectors').buttonset();
}
viewModel.selectedItem(item);
});
crossroads.addRoute("", function() {
viewModel.selectedItem("Apple");
})
});
</script>
...
ThisroutecontainsnosegmentsofanykindandwillmatchonlythebaseURL.ClickingtheBack
buttonuntilthebaseURLisreachednowcausestheapplicationtoreturntoitsinitialstate.(Well,it
returnssortofbacktoitsoriginalstate;laterinthischapterI’llexplainawrinkleinthisapproachand
showyouhowtoimproveuponit.)
AdaptingEvent-DrivenControlstoNavigation
Itisnotalwayspossibletolimittheelementsinadocumentsothatallnavigationcanbehandled
throughaelements.WhenaddingJavaScripteventstoaroutedapplication,Ifollowasimplepattern
thatbridgesbetweenURLroutingandconventionaleventsandthatgivesmealotofthebenefitsof
89
www.it-ebooks.info
CHAPTER4USINGURLROUTING
routingandletsmeuseotherkindsofelementsaswell.Listing4-9showsthispatternappliedtosome
otherelementtypes.
Listing4-9.BridgingBetweenURLRoutingandJavaScriptEvents
...
<script>
var viewModel = {
items: ko.observableArray(["Apple", "Orange", "Banana"]),
selectedItem: ko.observable("Apple")
};
$(document).ready(function() {
ko.applyBindings(viewModel);
$('div.catSelectors').buttonset();
hasher.initialized.add(crossroads.parse, crossroads);
hasher.changed.add(crossroads.parse, crossroads);
hasher.init();
crossroads.addRoute("select/:item:", function(item) {
if (!item) {
item = "Apple";
} else if (viewModel.items.indexOf(item)== -1) {
viewModel.items.push(item);
$('div.catSelectors').buttonset();
}
if (viewModel.selectedItem() != item) {
viewModel.selectedItem(item);
}
});
crossroads.addRoute("", function() {
viewModel.selectedItem("Apple");
})
$('[data-url]').live("change click", function(e) {
var target = $(e.target).attr("data-url");
if (e.target.tagName == 'SELECT') {
target += $(e.target).children("[selected]").val();
}
if (location.hash != target) {
location.replace(target);
}
})
});
</script>
...
Thetechniquehereistoaddadata-urlattributetotheelementswhoseeventsshouldresultina
navigationchange.IusejQuerytohandlethechangeandclickeventsforelementsthathavethedata-
90
www.it-ebooks.info
CHAPTER4USINGURLROUTING
urlattribute.Handlingbotheventsallowsmetocaterforthedifferentkindsofinputelements.Iusethe
livemethod,whichisaneatjQueryfeaturethatreliesoneventpropagationtoensurethateventsare
handledforelementsthatareaddedtothedocumentafterthescripthasexecuted;thisisessentialwhen
thesetofelementsinthedocumentcanbealteredinresponsetoviewmodelchanges.Thisapproach
allowsmetouseelementslikethis:
...
<div class="eventElemContainer" data-bind="foreach: items">
<label data-bind="attr: {for: $data}">
<span data-bind="text: $data"></span>
<input type="radio" name="item" data-bind="attr: {id: $data},
formatAttr: {attr: 'data-url', prefix: '#select/', value: $data}">
</label>
</div>
...
Thismarkupgeneratesasetofradiobuttonsforeachelementintheviewmodelitemsarray.I
createthevalueforthedata-urlattributewithmycustomformatAttrdatabinding,whichIdescribed
earlier.Theselectelementrequiressomespecialhandlingbecausewhiletheselectelementtriggers
thechangeevent,theinformationaboutwhichvaluehasbeenselectedisderivedfromthechildoption
elements.Hereissomemarkupthatcreatesaselectelementthatworkswiththispattern:
...
<div class="eventElemContainer">
<select name="eventItemSelect" data-bind="foreach: items,
attr: {'data-url': '#select/'}">
<option data-bind="value: $data, text: $data,
selected: $data == viewModel.selectedItem()">
</option>
</select>
</div>
...
PartofthetargetURLisinthedata-urlattributeoftheselectelement,andtherestistakenfrom
thevalueattributeoftheoptionelements.Someelements,includingselect,triggerboththeclickand
changeevents,soIchecktoseethatthetargetURLdiffersfromthecurrentURLbeforeusing
location.replacetotriggeranavigationchange.Listing4-10showshowthistechniquecanbeapplied
toselectelements,buttons,radiobuttons,andcheckboxes.
Listing4-10.BridgingBetweenEventsandRoutingforDifferentKindsofElements
<!DOCTYPE html>
<html>
<head>
<title>Routing Example</title>
<link rel="stylesheet" type="text/css" href="jquery-ui-1.8.16.custom.css"/>
<link rel="stylesheet" type="text/css" href="styles.css"/>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script src="jquery-ui-1.8.16.custom.js" type="text/javascript"></script>
<script src='knockout-2.0.0.js' type='text/javascript'></script>
<script src='utils.js' type='text/javascript'></script>
<script src='signals.js' type='text/javascript'></script>
<script src='crossroads.js' type='text/javascript'></script>
91
www.it-ebooks.info
CHAPTER4USINGURLROUTING
<script src='hasher.js' type='text/javascript'></script>
<script>
var viewModel = {
items: ko.observableArray(["Apple", "Orange", "Banana"]),
selectedItem: ko.observable("Apple")
};
$(document).ready(function() {
ko.applyBindings(viewModel);
$('div.catSelectors').buttonset();
hasher.initialized.add(crossroads.parse, crossroads);
hasher.changed.add(crossroads.parse, crossroads);
hasher.init();
crossroads.addRoute("select/:item:", function(item) {
if (!item) {
item = "Apple";
} else if (viewModel.items.indexOf(item)== -1) {
viewModel.items.push(item);
$('div.catSelectors').buttonset();
}
if (viewModel.selectedItem() != item) {
viewModel.selectedItem(item);
}
});
crossroads.addRoute("", function() {
viewModel.selectedItem("Apple");
})
$('[data-url]').live("change click", function(e) {
var target = $(e.target).attr("data-url");
if (e.target.tagName == 'SELECT') {
target += $(e.target).children("[selected]").val();
}
if (location.hash != target) {
location.replace(target);
}
})
});
</script>
</head>
<body>
<div class="catSelectors" data-bind="foreach: items">
<a data-bind="formatAttr: {attr: 'href', prefix: '#select/', value: $data},
css: {selectedItem: ($data == viewModel.selectedItem())}">
<span data-bind="text: $data"></span>
</a>
</div>
<div data-bind="foreach: items">
92
www.it-ebooks.info
CHAPTER4USINGURLROUTING
<div class="item" data-bind="fadeVisible: $data == viewModel.selectedItem()">
The selected item is: <span data-bind="text: $data"></span>
</div>
</div>
<div class="eventElemContainer">
<select name="eventItemSelect" data-bind="foreach: items,
attr: {'data-url': '#select/'}">
<option data-bind="value: $data, text: $data,
selected: $data == viewModel.selectedItem()">
</option>
</select>
</div>
<div class="eventElemContainer" data-bind="foreach: items">
<input type="button" data-bind="value: $data,
formatAttr: {attr: 'data-url', prefix: '#select/', value: $data}" />
</div>
<div class="eventElemContainer" data-bind="foreach: items">
<label data-bind="attr: {for: $data}">
<span data-bind="text: $data"></span>
<input type="checkbox" data-bind="attr: {id: $data},
formatAttr: {attr: 'data-url', prefix: '#select/', value: $data}">
</label>
</div>
<div class="eventElemContainer" data-bind="foreach: items">
<label data-bind="attr: {for: $data}">
<span data-bind="text: $data"></span>
<input type="radio" name="item" data-bind="attr: {id: $data},
formatAttr: {attr: 'data-url', prefix: '#select/', value: $data}">
</label>
</div>
</body>
</html>
Ihavedefinedanothercustombindingtocorrectlysettheselectedattributeontheappropriate
optionelement.Icalledthisbindingselected(obviouslyenough),anditisdefined,asshowninListing
4-11,intheutils.jsfile.
Listing4-11.TheSelectedDataBinding
ko.bindingHandlers.selected = {
init: function(element, accessor) {
if (accessor()) {
$(element).siblings("[selected]").removeAttr("selected");
$(element).attr("selected", "selected");
}
},
update: function(element, accessor) {
93
www.it-ebooks.info
CHAPTER4USINGURLROUTING
if (accessor()) {
$(element).siblings("[selected]").removeAttr("selected");
$(element).attr("selected", "selected");
}
}
}
Youmightbetemptedtosimplyhandleeventsandtriggertheapplicationchangesdirectly.This
works,butyouwillhavejustaddedtothecomplexityofyourapplicationbytakingontheoverheador
creatingandmanagingroutesandkeepingtrackofwhicheventsfromwhichelementstriggerdifference
statechanges.MyrecommendationistofocusonURLroutingandusebridging,asdescribedhere,to
funneleventsfromelementsintotheroutingsystem.
UsingtheHTML5HistoryAPI
TheCrossroadslibraryIhavebeenusingsofarinthischapterdependsontheHasherlibraryfromthe
sameauthortoreceivenotificationswhentheURLchanges.TheHasherlibrarymonitorstheURLand
tellsCrossroadswhenitchanges,triggeringtheroutingbehavior.
Thereisaweaknessinthisapproach,whichisthatthestateoftheapplicationisn’tpreservedaspart
ofthebrowserhistory.Herearesomestepstodemonstratetheissue:
1.
Loadthelistingintothebrowser.
2.
ClicktheOrangebutton.
3.
Navigatedirectlyto#select/Cherry.
4.
ClicktheBananabutton.
5.
ClicktheBackbuttontwice.
Everythingstartsoffwellenough.Whenyounavigatedtothe#select/CherryURL,thenewitemwas
addedtotheviewmodelandselectedproperly.WhenyouclickedtheBackbuttonthefirsttime,the
Cherryitemwascorrectlyselectedagain.TheproblemariseswhenyouclickedtheBackbuttonforthe
secondtime.TheselecteditemwascorrectlywoundbacktoOrange,buttheCherryitemremainedon
thelist.TheapplicationisabletousetheURLtoselectthecorrectitem,butwhentheOrangeitemwas
selectedoriginally,therewasnoCherryitemintheviewmodel,andyetitisstilldisplayedtotheuser.
Forsomewebapplications,thiswon’tbeabigdeal,anditisn’tforthissimpleexample,either.After
all,itdoesn’treallymatteriftheusercanselectanitemthattheyexplicitlyaddedinthefirstplace.But
forotherwebapps,thisisacriticalissue,andmakingsurethattheviewmodeliscorrectlypreservedin
thebrowserhistoryisessential.WecanaddressthisusingtheHTML5HistoryAPI,whichgivesusmore
accesstothebrowserhistorythanwebprogrammershavepreviouslyenjoyed.WeaccesstheHistoryAPI
throughthewindows.historyorglobalhistoryobject.TherearetwoaspectsoftheHistoryAPIthatIam
interestedinforthissituation.
94
www.it-ebooks.info
CHAPTER4USINGURLROUTING
NoteIamnotgoingtocovertheHTML5APIbeyondwhatisneededtomaintainapplicationstate.Iprovidefull
detailsinTheDefinitiveGuidetoHTML5,alsopublishedbyApress.YoucanreadtheW3Cspecificationat
http://dev.w3.org/html5/spec (theinformationontheHistoryAPIisinsection5.4,butthismaychangesince
theHTML5specificationisstillindraft).
Thehistory.replaceStatemethodletsyouassociateastateobjectwiththeentryinthebrowser’s
historyforthecurrentdocument.Therearethreeargumentstothismethod;thefirstisthestateobject,
thesecondargumentisthetitletouseinthehistory,andthethirdistheURLforthedocument.The
secondargumentisn’tusedbythecurrentgenerationofbrowsers,buttheURLargumentallowsyouto
effectivelyreplacetheURLinthehistorythatisassociatedwiththecurrentdocument.ThepartIam
interestedinforthischapteristhefirstargument,whichIwillusetostorethecontentsofthe
viewModel.itemsarrayinthehistorysothatIcanproperlymaintainthestatewhentheuserclicksthe
BackandForwardbuttons.
TipYoucanalsoinsertnewitemsintothehistoryusingthehistory.pushStatemethod.Thismethodtakes
thesameargumentsasreplaceStateandcanbeusefulforinsertingadditionalstateinformation.
Thewindowbrowserobjecttriggersapopstateeventwhenevertheactivehistoryentrychanges.If
theentryhasstateinformationassociatedwithit(becausethereplaceStateorpushStatemethodwas
used),thenyoucanretrievethestateobjectthroughthehistory.stateproperty.
AddingHistoryStatetotheExampleApplication
Thingsaren’tquiteassimpleasyoumightlikewhenitcomestousingtheHistoryAPI;itsuffersfrom
twoproblemsthatarecommontomostoftheHTML5APIs.Thefirstproblemisthatnotallbrowsers
supporttheHistoryAPI.Obviously,pre-HTML5browsersdon’tknowabouttheHistoryAPI,buteven
somebrowserversionsthatsupportotherHTML5featuresdonotimplementtheHistoryAPI.
ThesecondproblemisthatthosebrowsersthatdoimplementtheHTML5APIintroduce
inconsistencies,whichrequiressomecarefultesting.So,evenastheHistoryAPIhelpsussolveone
problem,wearefacedwithothers.Evenso,theHistoryAPIisworthusing,aslongasyouacceptthatit
isn’tuniversallysupportedandthatafallbackisrequired.Listing4-12showstheadditionoftheHistory
APItothesimpleexamplewebapp.
Listing4-12.UsingtheHTML5HistoryAPItoPreserveViewModelState
<!DOCTYPE html>
<html>
<head>
<title>Routing Example</title>
<link rel="stylesheet" type="text/css" href="jquery-ui-1.8.16.custom.css"/>
95
www.it-ebooks.info
CHAPTER4USINGURLROUTING
<link rel="stylesheet" type="text/css" href="styles.css"/>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script src="jquery-ui-1.8.16.custom.js" type="text/javascript"></script>
<script src="modernizr-2.0.6.js" type="text/javascript"></script>
<script src='knockout-2.0.0.js' type='text/javascript'></script>
<script src='utils.js' type='text/javascript'></script>
<script src='signals.js' type='text/javascript'></script>
<script src='crossroads.js' type='text/javascript'></script>
<script src='hasher.js' type='text/javascript'></script>
<script>
var viewModel = {
items: ko.observableArray(["Apple", "Orange", "Banana"]),
selectedItem: ko.observable("Apple")
};
$(document).ready(function() {
ko.applyBindings(viewModel);
$('div.catSelectors').buttonset();
crossroads.addRoute("select/:item:", function(item) {
if (!item) {
item = "Apple";
} else if (viewModel.items.indexOf(item)== -1) {
viewModel.items.push(item);
}
if (viewModel.selectedItem() != item) {
viewModel.selectedItem(item);
}
$('div.catSelectors').buttonset();
if (Modernizr.history) {
history.replaceState(viewModel.items(), document.title, location);
}
});
crossroads.addRoute("", function() {
viewModel.selectedItem("Apple");
})
if (Modernizr.history) {
$(window).bind("popstate", function(event) {
var state = history.state ? history.state
: event.originalEvent.state;
if (state) {
viewModel.items.removeAll();
$.each(state, function(index, item) {
viewModel.items.push(item);
});
}
96
www.it-ebooks.info
CHAPTER4USINGURLROUTING
crossroads.parse(location.hash.slice(1));
});
} else {
hasher.initialized.add(crossroads.parse, crossroads);
hasher.changed.add(crossroads.parse, crossroads);
hasher.init();
}
});
</script>
</head>
<body>
<div class="catSelectors" data-bind="foreach: items">
<a data-bind="formatAttr: {attr: 'href', prefix: '#select/', value: $data},
css: {selectedItem: ($data == viewModel.selectedItem())}">
<span data-bind="text: $data"></span>
</a>
</div>
<div data-bind="foreach: items">
<div class="item" data-bind="fadeVisible: $data == viewModel.selectedItem()">
The selected item is: <span data-bind="text: $data"></span>
</div>
</div>
</body>
</html>
StoringtheApplicationState
Thefirstsetofchangesinthelistingstorestheapplicationstatewhenthemainapplicationroute
matchesaURL.ByrespondingtotheURLchange,Iamabletopreservethestatewhenevertheuser
clicksoneofthenavigationelementsorentersaURLdirectly.Hereisthecodethatstoresthestate:
...
<script src="modernizr-2.0.6.js" type="text/javascript"></script>
...
crossroads.addRoute("select/:item:", function(item) {
if (!item) {
item = "Apple";
} else if (viewModel.items.indexOf(item)== -1) {
viewModel.items.push(item);
}
if (viewModel.selectedItem() != item) {
viewModel.selectedItem(item);
}
$('div.catSelectors').buttonset();
if (Modernizr.history) {
history.replaceState(viewModel.items(), document.title, location);
}
});
...
97
www.it-ebooks.info
CHAPTER4USINGURLROUTING
ThenewscriptelementinthelistingaddstheModernizrlibrarytothewebapp.Modernizrisa
feature-detectionlibrarythatcontainscheckstodeterminewhethernumerousHTML5andCSS3
featuresaresupportedbythebrowser.YoucandownloadModernizrandgetfulldetailsofthefeaturesit
candetectathttp://modernizr.com.
Idon’twanttocallthemethodsoftheHistoryAPIunlessIamsurethatthebrowserimplementsit,
soIcheckthevalueoftheModernizr.historyproperty.AvalueoftruemeansthattheHistoryAPIhas
beendetected,andavalueoffalsemeanstheAPIisn’tpresent.
Youcouldwriteyourownfeature-detectiontestsifyouprefer.Asanexample,hereisthecode
behindtheModernizr.historytest:
tests['history'] = function() {
return !!(window.history && history.pushState);
};
Modernizrsimplycheckstoseewhetherhistory.pushStateisdefinedbythebrowser.Iprefertouse
alibrarylikeModernizrbecausethetestsitperformsarewell-validatedandupdatedasneededand,
further,becausenotallofthetestsarequitesosimple.
TipFeature-detectionlibrariessuchasModernizrdon’tmakeanyassessmentofhowwellafeaturehasbeen
implemented.Thepresenceofthehistory.pushStatemethodindicatesthattheHistoryAPIispresent,butit
doesn’tprovideanyinsightsintoquirksorodditiesthatmayhavetobereckonedwith.Inshort,afeature-detection
libraryisnosubstituteforthoroughlytestingyourcodeonarangeofbrowsers.
IftheHistoryAPIispresent,thenIcallthereplaceStatemethodtoassociatethevalueoftheview
modelitemsarraywiththecurrentURL.IcanperformnoactioniftheHistoryAPIisn’tavailable
becausethereisn’tanalternativemechanismforstoringstateinthebrowser(althoughIcouldhave
usedapolyfill;seethesidebarfordetails).
USING A HISTORY POLYFILL
ApolyfillisaJavaScriptlibrarythatprovidessupportforanAPIforolderbrowsers.Pollyfilla,fromwhich
thenameoriginates,istheU.K.equivalentoftheSpacklehome-repairproduct,andtheideaisthata
polyfilllibrarysmoothesoutthedevelopmentlandscape.Polyfilllibrariescanalsoworkarounddifferences
betweenbrowserimplementationfeatures.TheHistoryAPImayseemlikeanidealcandidateforapolyfill,
buttheproblemisthatthebrowserdoesn’tprovideanyalternativemeansofstoringstateobjects.The
mostcommonworkaroundistoexpressthestateaspartoftheURLsothatwemightendupwith
somethinglikethis:
http://cheeselux.com/#select/Banana?items=Apple,Orange,Banana,Cherry
Idon’tlikethisapproachbecauseIdon’tliketoseecomplexdatatypesexpressedinthisway,andIthink
itproducesconfusingURLs.Butyoumightfeeldifferently,orastatefulhistoryfeaturemaybecriticalto
yourproject.Ifthat’sthecase,thenthebestHistoryAPIpolyfillthatIhavefoundiscalledHistory.jsandis
athttp://github.com/balupton/history.js.
98
www.it-ebooks.info
CHAPTER4USINGURLROUTING
RestoringtheApplicationState
Ofcourse,storingtheapplicationstateisn’tenough.Ialsohavetobeabletorestoreit,andthatmeans
respondingtothepopstateeventwhenitistriggeredbyaURLchange.Hereisthecode:
...
crossroads.addRoute("select/:item:", function(item) {
...other statements removed for brevity...
if (Modernizr.history) {
$(window).bind("popstate", function(event) {
var state = history.state ? history.state
: event.originalEvent.state;
if (state) {
viewModel.items.removeAll();
$.each(state, function(index, item) {
viewModel.items.push(item);
});
}
crossroads.parse(location.hash.slice(1));
});
} else {
hasher.initialized.add(crossroads.parse, crossroads);
hasher.changed.add(crossroads.parse, crossroads);
hasher.init();
}
});
...
IhaveusedModernizr.historytocheckfortheAPIbeforeIusethebindmethodtoregistera
handlerfunctionforthepopstateevent.Thisisn’tstrictlynecessarysincetheeventsimplywon’tbe
triggerediftheAPIisn’tpresent,butIliketomakeitobviousthatthisblockofcodeisrelatedtothe
HistoryAPI.
Youcanseeanexampleofcateringtoabrowseroddityinthefunctionthathandlesthepopstate
event.Thehistory.statepropertyshouldreturnthestateobjectassociatedwiththecurrentURL,but
GoogleChromedoesn’tsupportthis,andthevaluemustbeobtainedfromthestatepropertyofthe
Eventobjectinstead.jQuerynormalizesEventobjects,whichmeansthatIhavetousetheoriginalEvent
propertytogettotheunderlyingeventobjectthatthebrowsergenerated,likethis:
var state = history.state ? history.state: event.originalEvent.state;
WiththisapproachIcangetthestatedatafromhistory.stateifitisavailableandtheeventifitis
not.Sadly,usingtheHTML5APIsoftenrequiresthiskindofworkaround,althoughIexpectthe
consistencyofthevariousimplementationswillimproveovertime.
Ican’trelyontherebeingastateobjecteverytimethepopstateeventistriggeredbecausenotall
entriesinthebrowserhistorywillhavestateassociatedwiththem.
Whenthereisstatedata,IusetheremoveAllmethodtocleartheitemsarrayintheviewmodeland
thenpopulateitwiththeitemsobtainedfromthestatedatausingthejQueryeachfunction:
99
www.it-ebooks.info
CHAPTER4USINGURLROUTING
if (state) {
viewModel.items.removeAll();
$.each(state, function(index, item) {
viewModel.items.push(item);
});
}
Oncethecontentoftheviewmodelhasbeenset,InotifyCrossroadsthattherehasbeenachangein
URLbycallingtheparsemethod.ThiswasthefunctionpreviouslyhandledbytheHasherlibrary,which
removedtheleading#characterfromURLsbeforepassingthemtoCrossroads.Idothesameto
maintaincompatibilitywiththeroutesIdefinedearlier:
crossroads.parse(location.hash.slice(1));
IwanttopreservecompatibilitybecauseIdon’twanttoassumethattheuserhasanHTML5
browserthatsupportstheHistoryAPI.Tothatend,iftheModernizr.historypropertyisfalse,Ifallback
tousingHashersothatthebasicfunctionalityofthewebappstillworks,evenifIcan’tprovidethestate
managementfeature:
if (Modernizr.history) {
...History API code...
} else {
hasher.initialized.add(crossroads.parse, crossroads);
hasher.changed.add(crossroads.parse, crossroads);
hasher.init();
}
Withthesechanges,IamabletousetheHistoryAPIwhenitisavailabletomanagethestateofthe
applicationandunwinditwhentheuserusestheBackbutton.Figure4-6showsthekeystepfromthe
sequenceoftasksIhadyouperformatthestartofthissection.Astheusermovesbackthroughthe
history,theCherryitemdisappears.
Figure4-6.UsingtheHistoryAPItomanagechangesinapplicationstate
Asanaside,IchosetostoretheapplicationstateeverytimetheURLchangedbecauseitallowsme
tosupporttheForwardbuttonaswellastheBackbutton.Fromthestateshowninthefigure,clicking
theForwardbuttonrestorestheCherryitemtotheviewmodel,demonstratingthattheapplicationstate
isproperlypreservedandrestoredinbothdirections.
100
www.it-ebooks.info
CHAPTER4USINGURLROUTING
AddingURLRoutingtotheCheeseLuxWebApp
IswitchedtoasimpleexampleinthischapterbecauseIdidn’twanttooverwhelmtheroutingcode
(whichisprettysparse)withthemarkupanddatabindings(whichcanbeverbose).ButnowthatIhave
explainedhowURLroutingworks,itistimetointroduceittotheCheeseLuxdemo,asshownin
Listing4-13.
Listing4-13.AddingRoutingtotheCheeseLuxExample
<!DOCTYPE html>
<html>
<head>
<title>CheeseLux</title>
<link rel="stylesheet" type="text/css" href="styles.css"/>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script src="jquery-ui-1.8.16.custom.js" type="text/javascript"></script>
<script src='knockout-2.0.0.js' type='text/javascript'></script>
<script src='utils.js' type='text/javascript'></script>
<script src='signals.js' type='text/javascript'></script>
<script src='crossroads.js' type='text/javascript'></script>
<link rel="stylesheet" type="text/css" href="jquery-ui-1.8.16.custom.css"/>
<noscript>
<meta http-equiv="refresh" content="0; noscript.html"/>
</noscript>
<script>
var cheeseModel = {
products: [
{category: "British Cheese", items : [
{id: "stilton", name: "Stilton", price: 9},
{id: "stinkingbishop", name: "Stinking Bishop", price: 17},
{id: "cheddar", name: "Cheddar", price: 17}]},
{category: "French Cheese", items: [
{id: "camembert", name: "Camembert", price: 18},
{id: "tomme", name: "Tomme de Savoie", price: 19},
{id: "morbier", name: "Morbier", price: 9}]},
{category: "Italian Cheese", items: [
{id: "gorgonzola", name: "Gorgonzola", price: 8},
{id: "fontina", name: "Fontina", price: 11},
{id: "parmesan", name: "Parmesan", price: 16}]}]
};
$(document).ready(function() {
$('#buttonDiv input:submit').button().css("font-family", "Yanone");
cheeseModel.selectedCategory =
ko.observable(cheeseModel.products[0].category);
mapProducts(function(item) {
item.quantity = ko.observable(0);
item.subtotal = ko.computed(function() {
return this.quantity() * this.price;
101
www.it-ebooks.info
CHAPTER4USINGURLROUTING
}, item);
item.quantity.subscribe(function() {
updateState();
});
}, cheeseModel.products, "items");
cheeseModel.total = ko.computed(function() {
var total = 0;
mapProducts(function(elem) {
total += elem.subtotal();
}, cheeseModel.products, "items");
return total;
});
$('div.cheesegroup').not("#basket").css("width", "50%");
$('div.navSelectors').buttonset();
ko.applyBindings(cheeseModel);
$(window).bind("popstate", function(event) {
var state = history.state ? history.state : event.originalEvent.state;
restoreState(state);
crossroads.parse(location.hash.slice(1));
});
crossroads.addRoute("category/:newCat:", function(newCat) {
cheeseModel.selectedCategory(newCat ?
newCat : cheeseModel.products[0].category);
updateState();
});
crossroads.addRoute("remove/{id}", function(id) {
mapProducts(function(item) {
if (item.id == id) {
item.quantity(0);
}
}, cheeseModel.products, "items");
});
$('#basketTable a')
.button({icons: {primary: "ui-icon-closethick"},text: false});
function updateState() {
var state = {
category: cheeseModel.selectedCategory()
};
mapProducts(function(item) {
if (item.quantity() > 0) {
state[item.id] = item.quantity();
}
}, cheeseModel.products, "items");
history.replaceState(state, "",
102
www.it-ebooks.info
CHAPTER4USINGURLROUTING
}
"#select/" + cheeseModel.selectedCategory());
function restoreState(state) {
if (state) {
mapProducts(function(item) {
item.quantity(state[item.id] ? state[item.id] : 0);
}, cheeseModel.products, "items");
cheeseModel.selectedCategory(state.category);
}
}
});
</script>
</head>
<body>
<div id="logobar">
<img src="cheeselux.png">
<span id="tagline">Gourmet European Cheese</span>
</div>
<div class="cheesegroup">
<div class="navSelectors" data-bind="foreach: products">
<a data-bind="formatAttr: {attr: 'href', prefix: '#category/',
value: category},
css: {selectedItem: (category == cheeseModel.selectedCategory())}">
<span data-bind="text: category">
</a>
</div>
</div>
<div id="basket" class="cheesegroup basket">
<div class="grouptitle">Basket</div>
<div class="groupcontent">
<div class="description" data-bind="ifnot: total">
No products selected
</div>
<table id="basketTable" data-bind="visible: total">
<thead><tr><th>Cheese</th><th>Subtotal</th><th></th></tr></thead>
<tbody data-bind="foreach: products">
<!-- ko foreach: items -->
<tr data-bind="visible: quantity, attr: {'data-prodId': id}">
<td data-bind="text: name"></td>
<td>$<span data-bind="text: subtotal"></span></td>
<td>
<a data-bind="formatAttr: {attr: 'href',
prefix: '#remove/', value: id}"></a>
</td>
</tr>
<!-- /ko -->
</tbody>
103
www.it-ebooks.info
CHAPTER4USINGURLROUTING
<tfoot>
<tr><td class="sumline" colspan=2></td></tr>
<tr>
<th>Total:</th><td>$<span data-bind="text: total"></span></td>
</tr>
</tfoot>
</table>
</div>
<div class="cornerplaceholder"></div>
<div id="buttonDiv">
<input type="submit" value="Submit Order"/>
</div>
</div>
<form action="/shipping" method="post">
<!-- ko foreach: products -->
<div class="cheesegroup"
data-bind="fadeVisible: category == cheeseModel.selectedCategory()">
<div class="grouptitle" data-bind="text: category"></div>
<div data-bind="foreach: items">
<div class="groupcontent">
<label data-bind="attr: {for: id}" class="cheesename">
<span data-bind="text: name">
</span> $(<span data-bind="text:price"></span>)</label>
<input data-bind="attr: {name: id}, value: quantity"/>
<span data-bind="visible: subtotal" class="subtotal">
($<span data-bind="text: subtotal"></span>)
</span>
</div>
</div>
</div>
<!-- /ko -->
</form>
</body>
</html>
Iamnotgoingtobreakthislistingdownlinebylinebecausemuchoffunctionalityissimilarto
previousexamples.Thereare,however,acoupleoftechniquesthatareworthlearningandsome
changesthatIneedtoexplain,allofwhichI’llcoverinthesectionsthatfollow.Figure4-7showshowthe
webappappearsinthebrowser.
104
www.it-ebooks.info
CHAPTER4USINGURLROUTING
Figure4-7.AddingroutingtotheCheeseLuxexample
MovingthemapProductsFunction
Thefirstchange,andthemostbasic,isthatIhavemovedthemapProductsfunctionintotheutil.jsfile.
InChapter9,Iamgoingtoshowyouhowtopackageupthiskindoffunctionmoreusefully,andIdon’t
wanttokeeprecyclingthesamecodeinthelistings.AsImovedthefunction,Irewroteitsothatitcan
workonanysetofnestedarrays.Listing4-14showsthenewversionofthisfunction.
Listing4-14.TheRevisedmapProductsFunction
function mapProducts(func, data, indexer) {
$.each(data, function(outerIndex, outerItem) {
$.each(outerItem[indexer], function(itemIndex, innerItem) {
func(innerItem, outerItem);
});
});
}
Thetwonewargumentstothefunctionaretheouternestedarrayandthepropertynameofthe
innerarray.YoucanseehowIhaveusedthisinthemainlistingsothattheargumentsare
cheeseModel.productsanditems,respectively.
EnhancingtheViewModel
Imadetwochangestotheviewmodel.Thefirstwastodefineanobservabledataitemtocapturethe
selectedcheesecategory:
cheeseModel.selectedCategory = ko.observable(cheeseModel.products[0].category);
105
www.it-ebooks.info
CHAPTER4USINGURLROUTING
Thesecondismuchmoreinteresting.Databindingsarenotthemeansbywhichviewmodel
changesarepropagatedintothewebapp.Youcanalsosubscribetoanobservabledataitemandspecify
afunctionthatwillbeexecutedwhenthevaluechanges.HereisthesubscriptionIcreated:
mapProducts(function(item) {
item.quantity = ko.observable(0);
item.subtotal = ko.computed(function() {
return this.quantity() * this.price;
}, item);
item.quantity.subscribe(function() {
updateState();
});
}, cheeseModel.products, "items");
Isubscribedtothequantityobservableoneachcheeseproduct.Whenthevaluechanges,the
updateStatefunctionwillbeexecuted.I’lldescribethisfunctionshortly.Subscriptionsareratherlike
eventsfortheviewmodel;theycanbeusefulinanynumberofsituations,andIoftenfindmyselfusing
themwhenIwantsometaskperformedautomatically.
ManagingApplicationState
Iwanttopreservetwokindsofstateinthiswebapp.Thefirstistheselectedproductcategory,andthe
secondisthecontentsofthebasket.Istorestateinformationinthebrowser’shistoryintheupdateState
function,whichisexecutedwhenevermyquantitysubscriptionistriggeredortheselectedcategory
changes.
TipThetechniquethatIdemonstratehereisalittleoddwhenappliedtoashoppingbasket,becausewebsites
willusuallygotogreatlengthstopreserveyourproductselections.Ignorethis,ifyouwill,andfocusonthestate
managementtechnique,whichistherealpurposeofthissection.
function updateState() {
var state = {
category: cheeseModel.selectedCategory()
};
mapProducts(function(item) {
if (item.quantity() > 0) {
state[item.id] = item.quantity();
}
}, cheeseModel.products, "items");
history.replaceState(state, "", "#select/" + cheeseModel.selectedCategory());
}
106
www.it-ebooks.info
CHAPTER4USINGURLROUTING
TipThislistingrequirestheHTML5HistoryAPI,andunliketheearlierexamplesinthischapter,thereisno
fallbacktotheHTML4-compatibleapproachtakenbytheHasherlibrary.
Icreateanobjectthathasacategorypropertythatcontainsthenameoftheselectedcategoryand
onepropertyforeachindividualcheesethathasanonzeroquantityvalue.Iwritethistothebrowser
historyusingthereplaceStatemethod,whichIhavehighlightedinthelisting.
Somethingcleverishappeninghere.ToexplainwhatIamdoing—andwhy—wehavetostartwith
themarkupforthenavigationelementsthatremoveproductsfromthebasket.Hereistherelevant
HTML:
<a data-bind="formatAttr: {attr: 'href', prefix: '#remove/', value: id}"></a>
Whenthedatabindingsareapplied,Iendupwithanelementlikethis:
<a href="#/remove/stilton"></a>
InChapter3,Iremoveditemsfromthebasketbyhandlingtheclickeventfromtheseelements.
NowthatIamusingURLrouting,Ihavetodefinearoute,whichIdolikethis:
crossroads.addRoute("remove/{id}", function(id) {
mapProducts(function(item) {
if (item.id == id) {
item.quantity(0);
}
}, cheeseModel.products, "items");
});
Myroutematchesanytwo-segmentURLwherethefirstsegmentisremove.Iusethesecond
segmenttofindtherightitemintheviewmodelandchangethevalueofthequantitypropertytozero.
Atthispoint,Ihaveaproblem.IhavenavigatedtoaURLthatIdon’twanttheusertobeableto
navigatebacktobecauseitwillmatchtheroutethatjustremovesitemsfromthebasket,andthat
doesn’thelpme.
Thesolutionisinthecalltothehistory.replaceStatemethod.Whenthequantityvalueischanged,
mysubscriptioncausestheupdateStatefunctiontobecalled,whichinturncallshistory.replaceState.
Thethirdargumentistheimportantone:
history.replaceState(state, "", "#select/" + cheeseModel.selectedCategory());
TheURLspecifiedbythisargumentisusedtoreplacetheURLthattheusernavigatedto.The
browserdoesn’tnavigatetotheURLwhenitischanged,butwhentheusermovesbackthroughthe
browserhistory,itisthereplacementURLthatwillbeusedbythebrowser.Irrespectiveofwhichroute
matchestheURL,thehistorywillalwayscontainonethatstartswith#select/.Inthisway,IcanuseURL
routingwithoutexposingtheinnerworkingsofmywebapptotheuser.
107
www.it-ebooks.info
CHAPTER4USINGURLROUTING
Summary
Inthischapter,IhaveshownyouhowtoaddURLroutingtoyourwebapplications.Thisisapowerful
andflexibletechniquethatseparatesapplicationnavigationfromHTMLelements,allowingforamore
conciseandexpressivewayofhandlingnavigationandamoretestableandmaintainablecodebase.It
cantakeawhiletogetusedtousingroutingattheclient,butitiswellworththeinvestmentoftimeand
energy,especiallyforlargeandcomplexprojects.
108
www.it-ebooks.info
CHAPTER 5
Creating Offline Web Apps
TheHTML5specificationincludessupportfortheApplicationCache,whichisusedtocreateweb
applicationsthatareavailabletousersevenwhennonetworkconnectionisavailable.Thisisidealif
yourusersneedtoworkofflineorinenvironmentswhereconnectivityisconstrained(suchasonan
airplane,forexample).
AswithallofthemorecomplexHTML5features,usingtheapplicationcacheisn’tentirelysmooth
sailing.Therearesomedifferencesinimplementationsbetweenbrowsersandsomeodditiesthatyou
needtobeawareof.Inthischapter,I’llshowyouhowtocreateaneffectiveofflinewebapplicationand
howtoavoidvariouspitfalls.
CautionThebrowsersupportforofflinestorageisatanearlystage,andtherearealotofinconsistencies.I
havetriedtopointoutpotentialproblems,butbecauseeachbrowserreleasetendstorefinetheimplementationof
HTML5features,youshouldexpecttoseesomevariationswhenyouruntheexamplesinthischapter.
ResettingtheExample
Onceagain,IamgoingtosimplifytheCheeseLuxexamplesothatIamnotlistingreamsofcodethat
relatetootherchapters.Listing5-1showsthereviseddocument.
Listing5-1.TheResetCheeseLuxExample
<!DOCTYPE html>
<html>
<head>
<title>CheeseLux</title>
<link rel="stylesheet" type="text/css" href="styles.css"/>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script src="jquery-ui-1.8.16.custom.js" type="text/javascript"></script>
<script src='knockout-2.0.0.js' type='text/javascript'></script>
<script src='utils.js' type='text/javascript'></script>
<script src='signals.js' type='text/javascript'></script>
<script src='hasher.js' type='text/javascript'></script>
<script src='crossroads.js' type='text/javascript'></script>
<link rel="stylesheet" type="text/css" href="jquery-ui-1.8.16.custom.css"/>
109
www.it-ebooks.info
CHAPTER5CREATINGOFFLINEWEBAPPS
<noscript>
<meta http-equiv="refresh" content="0; noscript.html"/>
</noscript>
<script>
var cheeseModel = {
products: [
{category: "British Cheese", items : [
{id: "stilton", name: "Stilton", price: 9},
{id: "stinkingbishop", name: "Stinking Bishop", price: 17},
{id: "cheddar", name: "Cheddar", price: 17}]},
{category: "French Cheese", items: [
{id: "camembert", name: "Camembert", price: 18},
{id: "tomme", name: "Tomme de Savoie", price: 19},
{id: "morbier", name: "Morbier", price: 9}]},
{category: "Italian Cheese", items: [
{id: "gorgonzola", name: "Gorgonzola", price: 8},
{id: "fontina", name: "Fontina", price: 11},
{id: "parmesan", name: "Parmesan", price: 16}]}]
};
$(document).ready(function() {
$('#buttonDiv input:submit').button();
$('div.navSelectors').buttonset();
enhanceViewModel();
ko.applyBindings(cheeseModel);
hasher.initialized.add(crossroads.parse, crossroads);
hasher.changed.add(crossroads.parse, crossroads);
hasher.init();
crossroads.addRoute("category/:cat:", function(cat) {
cheeseModel.selectedCategory(cat || cheeseModel.products[0].category);
});
});
</script>
</head>
<body>
<div id="logobar">
<img src="cheeselux.png">
<span id="tagline">Gourmet European Cheese</span>
</div>
<div class="cheesegroup">
<div class="navSelectors" data-bind="foreach: products">
<a data-bind="formatAttr: {attr: 'href', prefix: '#category/',
value: category},
css: {selectedItem: (category == cheeseModel.selectedCategory())}">
<span data-bind="text: category">
</a>
</div>
110
www.it-ebooks.info
CHAPTER5CREATINGOFFLINEWEBAPPS
</div>
<form action="/shipping" method="post">
<div data-bind="foreach: products">
<div class="cheesegroup"
data-bind="fadeVisible: category == cheeseModel.selectedCategory()">
<div class="grouptitle" data-bind="text: category"></div>
<!-- ko foreach: items -->
<div class="groupcontent">
<label data-bind="attr: {for: id}" class="cheesename">
<span data-bind="text: name">
</span> $(<span data-bind="text:price"></span>)</label>
<input data-bind="attr: {name: id}, value: quantity"/>
<span data-bind="visible: subtotal" class="subtotal">
($<span data-bind="text: subtotal"></span>)
</span>
</div>
<!-- /ko -->
<div class="groupcontent">
<label class="cheesename">Total:</label>
<span class="subtotal" id="total">
$<span data-bind="text: cheeseModel.total()"></span>
</span>
</div>
</div>
</div>
<div id="buttonDiv">
<input type="submit" value="Submit Order"/>
</div>
</form>
</body>
</html>
Thisexamplebuildsontheviewmodelandroutingconceptsfrompreviouschapters,butIhave
simplifiedsomeofthefunctionality.Insteadofabasket,Ihaveaddedatotaldisplaytothebottomof
eachcategoryofcheese.Ihavemovedthecodethatcreatestheobservableviewmodelitemsintoa
functioncalledenhanceViewModelintheutils.jsfile.Everythingelseinthislistingshouldbeselfevident.
UsingtheHTML5ApplicationCache
Thestartingpointforusingtheapplicationcacheistocreateamanifest.Thistellsthebrowserwhich
filesarerequiredtoruntheapplicationofflinesothatthebrowsercanensurethattheyareallpresentin
thecache.Manifestfileshavetheappcachefilesuffix,soIhavecalledmymanifestfile
cheeselux.appcache.YoucanseethecontentsofthisfileinListing5-2.
111
www.it-ebooks.info
CHAPTER5CREATINGOFFLINEWEBAPPS
Listing5-2.ASimpleManifestFile
CACHE MANIFEST
# HTML document
example.html
offline.html
# script files
jquery-1.7.1.js
jquery-ui-1.8.16.custom.js
knockout-2.0.0.js
signals.js
crossroads.js
hasher.js
utils.js
# CSS files
styles.css
jquery-ui-1.8.16.custom.css
# images
#blackwave.png
cheeselux.png
images/ui-bg_flat_75_eb8f00_40x100.png
images/ui-bg_flat_75_fbbe03_40x100.png
images/ui-icons_ffffff_256x240.png
images/ui-bg_flat_75_595959_40x100.png
images/ui-bg_flat_65_fbbe03_40x100.png
# fonts
fonts/YanoneKaffeesatz-Regular.ttf
fonts/fanwood_italic-webfont.ttf
fonts/ostrich-rounded-webfont.woff
AbasicmanifestfilestartswiththeCACHE MANIFESTheaderandthenlistsallthefilesthatthe
applicationrequires,includingtheHTMLfilewhosehtmlelementcontainsthemanifestattribute
(discussedinamoment).Inthelisting,Ihavebrokenthefilesdownbytypeandusedcomments(which
arelinesstartingwiththe#character)tomakeiteasiertofigureoutwhat’shappening.
TipYouwillnoticethatIhavecommentedouttheentryfortheblackwave.pngfile.Iusethisfileto
demonstratethebehaviorofacachedapplicationinamoment.
ThemanifestisaddedtotheHTMLdocumentthroughthemanifestattributeofthehtmlelement,
asListing5-3shows.
112
www.it-ebooks.info
CHAPTER5CREATINGOFFLINEWEBAPPS
Listing5-3.AddingtheManifesttotheHTMLDocument
<!DOCTYPE html>
<html manifest="cheeselux.appcache">
<head>
...
</head>
<body>
...
</body>
</html>
WhentheHTMLdocumentisloaded,thebrowserdetectsthemanifestattribute,requeststhe
specifiedappcachefilefromthewebserver,andbeginsloadingandcachingeachfilelistedinthe
manifestfile.Thefilesthataredownloadedwhenthebrowserprocessesthemanifestarecalledthe
offlinecontent.Somebrowserswillprompttheuserforpermissiontostoreofflinecontent.
CautionBecarefulwhenyoucreatethemanifest.Ifanyoftheitemslistedcannotbeobtainedfromtheserver,
thenthebrowserwillnotcachetheapplicationatall.
UnderstandingWhenCachedContentIsUsed
Theofflinecontentisn’tusedwhenitisfirstloadedbythebrowser.Itiscachedforthenexttimethatthe
userloadsorreloadsthepage.Thenameofflinecontentismisleading.Oncethebrowserhasoffline
contentforawebapp,itwillbeusedwhenevertheuservisitsthewebapp’sURL,evenwhenthereisa
networkconnectionavailable.Thebrowsertakesresponsibilityforensuringthatthelatestversionofthe
offlinecontentisbeingused,butasyou’lllearn,thisisacomplicatedprocessandrequiressome
programmerintervention.
Icommentedouttheblackwave.pngfileinthemanifesttodemonstratehowthebrowserhandles
offlinecontent.Iuseblackwave.pngasthebackgroundimagefortheCheeseLuxwebapp,andthisgives
meanicewaytodemonstratethebasicbehaviorofacachedwebapplication.
Tostartwith,addthemanifestattributetotheexampleasshowninListing5-3,andloadthe
documentintoyourbrowser.Differentbrowsersdealwithcachedapplicationsindifferentways.For
example,GoogleChromewillquietlyprocessthemanifestandstartdownloadingthecontentit
specified.MozillaFirefoxwillusuallyprompttheusertoallowofflinecontent,asshowninFigure5-1.If
youareusingFirefox,clicktheAllowbuttontostartthebrowserprocessingthemanifest.
113
www.it-ebooks.info
CHAPTER5CREATINGOFFLINEWEBAPPS
Figure5-1.Firefoxpromptingtheusertoallowthewebapptostoredatalocally
TipAllofthemainstreambrowsersallowtheusertodisablecachedapplications,whichmeansyoucannotrely
onbeingabletostoredataevenifthebrowserimplementsthefeature.Insuchcases,theapplicationmanifestwill
simplybeignored.Youmayneedtochangetheconfigurationofyourbrowsertocachetheexamplecontent.
YoushouldseetheCheeseLuxwebappwiththeblackbackground.Atthispoint,thebrowserhas
twocopiesofthewebapp.Thefirstcopyisintheregularbrowsercache,andthisistheversionthatis
currentlyrunning.Thesecondcopyisintheapplicationcacheandcontainstheitemsspecifiedinthe
manifest.Simplyreloadthepagetoswitchtotheapplicationcacheversion.Whenyoudoreload,the
backgroundwillbewhite,asshowninFigure5-2.
Figure5-2.Switchingtotheapplicationcache
Thedifferenceiscausedbythefactthattheblackwave.pngfileiscommentedoutinthemanifest.
Thebrowserkeepstheapplicationcacheandtheregularcacheseparate,whichmeansthateventhough
ithasablackwave.pngfileintheregularcache,itwon’tuseitforacachedapplication.
TipNoticethatyouhavenotdoneanythingtothenetworkconnection.Thebrowserisstillonline,butthe
applicationhasbeenloadedusingsolelyofflinecontent.ThisissomethingthatI’llreturntosoon.
114
www.it-ebooks.info
CHAPTER5CREATINGOFFLINEWEBAPPS
AcceptingChangestotheManifest
Themostsignificantchangeinbehaviorforacachedapplicationisthatrefreshingthewebpagedoesn’t
causetheapplicationcontenttobecached.Theideaisthatupdatestoacachedapplicationneedtobe
managedtoavoidinconsistentchanges.Uncommentingtheblackwave.pnglineinthemanifestand
reloading,forexample,wouldn’tchangethebackgroundtoblack.
Listing5-4showstheminimumamountofcodethatisneededinawebapptosupportupdates.I’ll
showyouhowtousemoreoftheApplicationCacheAPIlaterinthechapter,butweneedthesechanges
beforewecangoanyfurther.
Listing5-4.AcceptingChangesintheManifest
...
<script>
var cheeseModel = {
products: [
{category: "British Cheese", items : [
{id: "stilton", name: "Stilton", price: 9},
{id: "stinkingbishop", name: "Stinking Bishop", price: 17},
{id: "cheddar", name: "Cheddar", price: 17}]},
{category: "French Cheese", items: [
{id: "camembert", name: "Camembert", price: 18},
{id: "tomme", name: "Tomme de Savoie", price: 19},
{id: "morbier", name: "Morbier", price: 9}]},
{category: "Italian Cheese", items: [
{id: "gorgonzola", name: "Gorgonzola", price: 8},
{id: "fontina", name: "Fontina", price: 11},
{id: "parmesan", name: "Parmesan", price: 16}]}]
};
$(document).ready(function() {
$('#buttonDiv input:submit').button();
$('div.navSelectors').buttonset();
enhanceViewModel();
ko.applyBindings(cheeseModel);
hasher.initialized.add(crossroads.parse, crossroads);
hasher.changed.add(crossroads.parse, crossroads);
hasher.init();
crossroads.addRoute("category/:cat:", function(cat) {
cheeseModel.selectedCategory(cat || cheeseModel.products[0].category);
});
$(window.applicationCache).bind("updateready", function() {
window.applicationCache.swapCache();
});
});
</script>
115
www.it-ebooks.info
CHAPTER5CREATINGOFFLINEWEBAPPS
...
TheHTML5ApplicationCacheAPIisexpressedthroughthewindow.applicationCachebrowser
object.Thisobjecttriggerseventstoinformthewebappofchangesinthecachestatus.Themost
importantforusatthemomentistheupdatereadyevent,whichmeansthatthereisupdatedcachedata
available.Inadditiontotheevents,theapplicationCacheobjectdefinessomeusefulmethodsand
properties.Onceagain,I’llreturntotheselaterinthechapter,butthemethodIcareaboutnowis
swapCache,whichappliestheupdatedmanifestanditscontentstotheapplicationcache.
Iamnowreadytodemonstrateupdatingacachedwebapplication.ButbeforeIdo,Imustremove
theexistingcacheddata.Ihavecreatedazombiewebappbyapplyingamanifestwithoutaddingthecall
totheswapCachemethod,andthereisnowayIcangetupdatestotakeeffect.Ineedtoclearthecache
andstartagain.ThereisnowaytoclearthecacheusingJavaScript,andthebrowserhasadifferent
mechanismformanuallyclearingapplicationcachedata.ForGoogleChrome,youdeletetheregular
browsinghistory.ForMozillaFirefox,youmustselecttheAdvanced➤Networkoptionstab,selectthe
websitefromthelist,andclicktheRemovebutton.
Onceyouhaveclearedtheapplicationcache,reloadthelistingtoloadthemanifestandcachethe
data.Reloadthepageagaintoswitchtothecachedversionoftheapplication(whichwillhavethewhite
background).
Finally,youcanuncommenttheblackwave.pngentryinthecheeselux.appcachefile.Atthispoint,
youwillneedtoreloadthewebpagetwice.Thefirsttimecausesthebrowsertocheckforanupdated
manifest,findthatthereisanewversion,anddownloadtheupdatedresourcesintothecache.Atthis
point,theupdatereadyeventistriggered,andmyscriptcallstheswapCachemethod,applyingtheupdates
tothecache.Thosechangesdon’ttakeeffectuntilthenexttimethatthewebappisloaded,whichiswhy
thesecondreloadisrequired.Thisisanawkwardapproach,butI’llshowyouhowtoimproveuponit
shortly.Atthispoint,thecachewillhavebeenupdatedwithamanifestthatdoesincludethe
blackwave.pngfile,andthewebappbackgroundwillhaveturnedblack.
TipThebrowsercheckstoseeonlyifthemanifestfilehaschanged.Changestoindividualresources,including
HTMLandscriptfiles,areignoredunlessthemanifestalsochanges.Ifthemanifesthaschanged,thenthe
browserwillchecktoseewhethertheindividualresourceshavebeenupdatedsincetheywerelastdownloaded
(and,ofcourse,willdownloadanyresourcesthathavebeenaddedtothemanifest).
TakingControloftheCacheUpdateProcess
ItookyouthelongwayaroundtheupdatesbecauseIwantedtoemphasizethewayinwhichthe
browsertriestoisolateusfromhavingtodealwithaninconsistentcache.Thereisnostandardwayfora
JavaScriptwebapptorespondtoacachechangewhileitisrunning,sotheHTML5ApplicationCache
standarderrsonthesideofcaution,andcacheupdatesareappliedonlywhentheapplicationisloaded.
116
www.it-ebooks.info
CHAPTER5CREATINGOFFLINEWEBAPPS
CautionThecurrentimplementationsoftheapplicationcachearefineforusebynormalusers,buttheytend
tostruggleduringthedevelopmentphasewhentherearelotsofchangestothemanifestandlotsofupdates
appliedtothecache.Therewillcomeapointwhereyoustartgettingoddbehavior,andnochangesyoumaketo
yourmanifestoryourapplicationwillsortmattersout.Whenthishappens,thesimplestthingtodoistoclearthe
browserhistoryandapplicationcachecontentsandseewhethertheproblemspersist.Mostofthetime,Ifindthat
suddenchangesinbehaviorarecausedbythebrowserandthatstartingoverfixesthings(althoughthis
sometimesrequiresclearingthefilesdirectlyfromthediskusingthefileexplorer,becausethebrowser’sabilityto
managetheapplicationcachealsogoesawry).
WecanusetheapplicationCachebrowserobjecttomanageacachedapplicationinamoreelegant
way.Thefirstthingwecandoistomonitorthestatusofthecacheandpresenttheuserwithsome
options.Listing5-5showshowthiscanbedone.
Listing5-5.TakingActiveControloftheApplicationCache
<!DOCTYPE html>
<html manifest="cheeselux.appcache">
<head>
<title>CheeseLux</title>
<link rel="stylesheet" type="text/css" href="styles.css"/>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script src="jquery-ui-1.8.16.custom.js" type="text/javascript"></script>
<script src='knockout-2.0.0.js' type='text/javascript'></script>
<script src='utils.js' type='text/javascript'></script>
<script src='signals.js' type='text/javascript'></script>
<script src='hasher.js' type='text/javascript'></script>
<script src='crossroads.js' type='text/javascript'></script>
<link rel="stylesheet" type="text/css" href="jquery-ui-1.8.16.custom.css"/>
<noscript>
<meta http-equiv="refresh" content="0; noscript.html"/>
</noscript>
<script>
var cheeseModel = {
products: [
{category: "British Cheese", items : [
{id: "stilton", name: "Stilton", price: 9},
{id: "stinkingbishop", name: "Stinking Bishop", price: 17},
{id: "cheddar", name: "Cheddar", price: 17}]},
{category: "French Cheese", items: [
{id: "camembert", name: "Camembert", price: 18},
{id: "tomme", name: "Tomme de Savoie", price: 19},
{id: "morbier", name: "Morbier", price: 9}]},
{category: "Italian Cheese", items: [
{id: "gorgonzola", name: "Gorgonzola", price: 8},
117
www.it-ebooks.info
CHAPTER5CREATINGOFFLINEWEBAPPS
{id: "fontina", name: "Fontina", price: 11},
{id: "parmesan", name: "Parmesan", price: 16}]}],
cache: {
status: ko.observable(window.applicationCache.status)
}
};
$(document).ready(function() {
$('#buttonDiv input:submit').button();
$('div.navSelectors').buttonset();
enhanceViewModel();
ko.applyBindings(cheeseModel);
hasher.initialized.add(crossroads.parse, crossroads);
hasher.changed.add(crossroads.parse, crossroads);
hasher.init();
crossroads.addRoute("category/:cat:", function(cat) {
cheeseModel.selectedCategory(cat || cheeseModel.products[0].category);
});
$(window.applicationCache).bind("checking noupdate downloading " +
"progress cached updateready", function(e) {
cheeseModel.cache.status(window.applicationCache.status);
});
$('div.tagcontainer a').button().click(function(e) {
e.preventDefault();
if ($(this).attr("data-action") == "update") {
window.applicationCache.update();
} else {
window.applicationCache.swapCache();
window.location.reload(false);
}
});
});
</script>
</head>
<body>
<div id="logobar">
<img src="cheeselux.png">
<div class="tagcontainer">
<span id="tagline">Gourmet European Cheese</span>
<div>
<a data-bind="visible: cheeseModel.cache.status() != 4"
data-action="update" class="cachelink">Check for Updates</a>
<a data-bind="visible: cheeseModel.cache.status() == 4"
data-action="swapCache" class="cachelink">Apply Update</a>
</div>
</div>
118
www.it-ebooks.info
CHAPTER5CREATINGOFFLINEWEBAPPS
</div>
<div class="cheesegroup">
<div class="navSelectors" data-bind="foreach: products">
<a data-bind="formatAttr: {attr: 'href', prefix: '#category/',
value: category},
css: {selectedItem: (category == cheeseModel.selectedCategory())}">
<span data-bind="text: category">
</a>
</div>
</div>
<form action="/shipping" method="post">
<div data-bind="foreach: products">
<div class="cheesegroup"
data-bind="fadeVisible: category == cheeseModel.selectedCategory()">
<div class="grouptitle" data-bind="text: category"></div>
<!-- ko foreach: items -->
<div class="groupcontent">
<label data-bind="attr: {for: id}" class="cheesename">
<span data-bind="text: name">
</span> $(<span data-bind="text:price"></span>)</label>
<input data-bind="attr: {name: id}, value: quantity"/>
<span data-bind="visible: subtotal" class="subtotal">
($<span data-bind="text: subtotal"></span>)
</span>
</div>
<!-- /ko -->
<div class="groupcontent">
<label class="cheesename">Total:</label>
<span class="subtotal" id="total">
$<span data-bind="text: cheeseModel.total()"></span>
</span>
</div>
</div>
</div>
<div id="buttonDiv">
<input type="submit" value="Submit Order"/>
</div>
</form>
</body>
</html>
Tostartwith,Ihaveaddedanewobservabledataitemtotheviewmodel,whichrepresentsthestate
oftheapplicationcache:
cache: {
status: ko.observable(window.applicationCache.status)
}
IamusingtheviewmodelbecauseIwanttodisseminatethestatusintotheHTMLmarkupusing
databindings.Tokeepthevalueup-to-date,Isubscribetoasetofeventstriggeredbythe
window.applicationCacheobject,likethis:
119
www.it-ebooks.info
CHAPTER5CREATINGOFFLINEWEBAPPS
$(window.applicationCache).bind("checking noupdate downloading " +
"progress cached updateready", function(e) {
cheeseModel.cache.status(window.applicationCache.status);
});
Sevencacheeventsareavailable.IhavelistedtheminTable5-1.Ihaveusedthebindmethodto
handlesixofthem,becausetheseventh,obsolete,arisesonlywhenthemanifestfileisn’tavailablefrom
thewebserver.
Table5-1.HTML5ApplicationCacheEvents
Event Name
Description
cached
Theinitialmanifestandcontentfortheapplicationhavebeendownloaded.
checking
Thebrowserischeckingforanupdatetothemanifestfile.
noupdate
Thebrowserhasfinishedcheckingthemanifest,andtherewerenoupdates.
downloading
Thebrowserisdownloadingupdatedofflinecontent.
progress
Usedbythebrowsertoindicatedownloadprogress.
updateready
Thecontentdownloadiscomplete,andthereisacacheupdateready.
obsolete
Themanifestisinvalid.
Iupdatethecache.statusdataitemintheviewmodelwhenIreceivedanapplicationcacheevent.
Thecurrentstatusisavailablefromthewindow.applicationCache.statusproperty,andIhavedescribed
therangeofvaluesthatarereturnedinTable5-2.
Table5-2.ValuesReturnedbytheapplicationCache.statusProperty
Value
Name
0 UNCACHED
1
IDLE
Description
Returnedforwebappsthatdonotspecifyamanifestorwhenthereisa
manifestbuttheofflinecontenthasnotbeendownloaded.
Thecacheisnotperforminganyaction.Thisisthedefaultvalueoncethe
offlinecontenthasbeendownloadedandcached.
2 CHECKING
Thebrowserischeckingforanupdatedmanifest.
3 DOWNLOADING
Thebrowserisdownloadingupdatedofflinecontent.
4 UPDATEREADY
Thereisupdatedofflinecontentwaitingtobeappliedtothecache.
5 OBSOLETE
Thecacheddataisobsolete.
120
www.it-ebooks.info
CHAPTER5CREATINGOFFLINEWEBAPPS
Asyoucansee,thestatusvaluescorrespondwithsomeoftheapplicationcacheevents.Forthis
example,IcareonlyabouttheUPDATEREADYstatusvalue,whichIusetocontrolthevisibilityofsomea
elementsIaddedtothelogoareaofthepage:
<div>
<a data-bind="visible: cheeseModel.cache.status() != 4"
data-action="update" class="cachelink">Check for Updates</a>
<a data-bind="visible: cheeseModel.cache.status() == 4"
data-action="swapCache" class="cachelink">Apply Update</a>
</div>
Whenthecacheisidle,Idisplaytheelementthatpromptstheusertocheckforanupdate,and
whenthereisanupdateavailable,Iprompttheusertoinstallit.Figure5-3showsbothofthesebuttons
insitu.
Figure5-3.Addingbuttonstocontrolthecache
Asyoucanseeinthefigure,IhaveusedjQueryUItocreatebuttonsfromtheaelements.Ihavealso
usedthejQueryclickmethodtoregisterahandlerfortheclickevent,asfollows:
$('div.tagcontainer a').button().click(function(e) {
e.preventDefault();
if ($(this).attr("data-action") == "update") {
window.applicationCache.update();
} else {
window.applicationCache.swapCache();
window.location.reload(false);
}
});
IhaveusedregularJavaScripteventstocontrolthecachebecauseIwanttheusertobeabletocheck
forupdatesrepeatedly.BrowsersignorerequeststonavigatetothesameinternalURLthatisbeing
displayed.Youcanseethishappeningifyouclickoneofthecheesecategorybuttons.Clickingthesame
buttonrepeatedlydoesn’tdoanything,andthebuttoniseffectivelydisableduntilanothercategoryis
selected.IfIhadusedURLroutingtodealwiththecachebuttons,thentheuserwouldbeabletocheck
foranupdateonceandthennotbeabletodosoagainuntiltheynavigatedtoanotherinternalURL
(whichforthisexamplewouldrequireselectingacheesecategory).So,instead,IusedJavaScriptevents
thataretriggeredeverytimethebuttonisclicked,irrespectiveoftherestoftheapplicationstate.
121
www.it-ebooks.info
CHAPTER5CREATINGOFFLINEWEBAPPS
Wheneithercachebuttonisclicked,Ireadthevalueofthedata-actionattribute.Iftheattribute
valueisupdate,thenIcallthecacheupdatemethod.Thiscausesthebrowsertocheckwiththeserverto
seewhetherthemanifesthaschanged.Ifithas,thenthestatusofthecachewillchangetoUPDATEREADY,
andtheApplyUpdatebuttonwillbeshowntotheuser.
WhentheApplyUpdatebuttonisclicked,IcalltheswapCachemethodtopushtheupdatesintothe
applicationcache.Theseupdateswon’ttakeeffectuntiltheapplicationisreloaded,whichIforceby
callingthewindow.location.reloadmethod.Thismeanstheupdatesareappliedtothecacheand
immediatelyusedinresponsetoasingleactionbytheuser.Thesimplestwaytotesttheseadditionsisto
togglethestatusoftheblackwave.pngimageinthemanifestandapplytheresultingupdate.Seethe
informationonthecachecontrolheaderifyouwanttotestmoresubstantialchanges.
APPLICATION CACHE ENTRIES AND THE CACHE-CONTROL HEADER
CallingtheapplicationCachemethoddoesn’talwayscausethebrowsertocontacttheservertosee
whetherthemanifesthaschanged.AllofthemainstreambrowsershonortheHTTPCache-Control
headerandwillcheckforupdatesonlywhenthelifeofthemanifesthasexpired.
Further,evenifthemanifesthaschanged,thebrowserhonorstheCache-Controlvalueforindividual
manifestitems.ThiscanleadtoasituationwhereanupdatetoanHTMLorscriptfileisignoredifthe
manifestchangeswithintheCache-Controllifetimeoftheaffectedresource.
Inproduction,thisbehaviorisperfectlyreasonable.Butduringdevelopmentandtesting,it’sahugepain
sincechangesmadetothecontentsofHTMLandscriptfileswon’tbeimmediatelyreflectedinanupdate.
Togetaroundthis,IhavesetaveryshortcachelifeonthecontentservedbytheNode.jsserver.You’ll
needtodosomethingsimilartoyourdevelopmentserverstogetthesameeffect.
AddingNetworkandFallbackEntriestotheManifest
Regularmanifestentriestellthebrowsertoproactivelyobtainandcacheresourcesthatthewebapp
requires.Inaddition,theapplicationcachesupportstwoothermanifestentrytypes:networkand
fallbackentries.Networkentries,alsoknownaswhitelistentries,specifyaresourcethatthebrowser
shouldnotcache.Requestsfortheseresourceswillalwaysresultinarequesttotheserverwhilethe
browserisonline.Thisisusefultoensurethattheuseralwaysreceivesthelatestversionofafile,even
thoughtherestoftheapplicationiscached.
Thefallbackentriestellthebrowserwhattodowhenthebrowserisofflineandtheuserrequestsa
networkentry.Fallbackentriesallowyoutosubstituteanalternativefileratherthandisplayinganerror
totheuser.Listing5-6showstheuseofbothkindsofentryinthecheeselux.appcachefile.
Listing5-6.UsingaNetworkEntryintheApplicationManifest
CACHE MANIFEST
# HTML document
example.html
# script files
jquery-1.7.1.js
jquery-ui-1.8.16.custom.js
122
www.it-ebooks.info
CHAPTER5CREATINGOFFLINEWEBAPPS
knockout-2.0.0.js
signals.js
crossroads.js
hasher.js
utils.js
# CSS files
styles.css
jquery-ui-1.8.16.custom.css
# images
blackwave.png
cheeselux.png
images/ui-bg_flat_75_eb8f00_40x100.png
images/ui-bg_flat_75_fbbe03_40x100.png
images/ui-icons_ffffff_256x240.png
images/ui-bg_flat_75_595959_40x100.png
images/ui-bg_flat_65_fbbe03_40x100.png
# fonts
fonts/YanoneKaffeesatz-Regular.ttf
fonts/fanwood_italic-webfont.ttf
fonts/ostrich-rounded-webfont.woff
NETWORK:
news.html
ThenetworkentriesareprefixedwiththewordNETWORKandacolon(:).Aswiththeregularentries,
eachresourceoccupiesasingleline.Inthislisting,Ihavecreatedanetworkentryforthefilenews.html.I
havecreatedabuttonthatlinkstothisfileintheexample.htmlfile,likethis:
<div id="logobar">
<img src="cheeselux.png">
<div class="tagcontainer">
<span id="tagline">Gourmet European Cheese</span>
<div>
<a data-bind="visible: cheeseModel.cache.status() != 4"
data-action="update" class="cachelink">Check for Updates</a>
<a data-bind="visible: cheeseModel.cache.status() == 4"
data-action="swapCache" class="cachelink">Apply Update</a>
<a class="cachelink" href="news.html">News</a>
</div>
</div>
</div>
Whenthebrowserisonline,clickingthislinkdisplaysthenews.htmlfile.Youcanseetheeffectin
Figure5-4.
123
www.it-ebooks.info
CHAPTER5CREATINGOFFLINEWEBAPPS
Figure5-4.Linkingtothenews.htmlpage
BecauseitisintheNETWORKsection,thenews.htmlfileisneveraddedtotheapplicationcache.When
IclicktheNewsbutton,thebrowseractsasitwouldforregularcontent.Itcontactstheserver,getsthe
resources,andaddsthemtotheregular(nonapplication)cache,beforeshowingthemtotheuser.Ican
makechangestothenews.htmlfile,andtheywillbedisplayedtotheuserevenwhentheapplication
cachehasn’tbeenupdated.
Whenthebrowsergoesoffline,thereisnowaytogetholdofthecontentthatisnotinthe
applicationcache.ThisiswheretheFALLBACKentriescomein.Theformatoftheseentriesisdifferent
fromtheothers.
CautionBrowserstakedifferentviewsaboutwhatbeingofflinemeans.Iexplainmoreaboutthisinthe
“MonitoringOfflineStatus”sectionlaterinthischapter.
Thefirstpartspecifiesaprefixforresources,andthesecondpartspecifiesafiletousewhena
resourcethatmatchestheprefixisrequestedwhilethebrowserisoffline.So,inListing5-7,Ihavesetthe
manifestsothatanyrequesttoanyURL(representedby/)shouldbegiventhefileoffline.htmlinstead.
124
www.it-ebooks.info
CHAPTER5CREATINGOFFLINEWEBAPPS
Listing5-7.UsingaFallbackEntryintheApplicationManifest
...
# fonts
fonts/YanoneKaffeesatz-Regular.ttf
fonts/fanwood_italic-webfont.ttf
fonts/ostrich-rounded-webfont.woff
FALLBACK:
/ offline.html
TipBrowsershandlefallbackforresourcesinthenetworkinconsistently.Youshouldnotrelyonthefallback
sectiontoprovidesubstitutecontentforURLsthatarelistedinthenetworksection,onlythosethatareinthemain
partofthemanifest.Supportforprovidingfallbacksforindividualfilesisalsoinconsistent,whichiswhyIhave
usedthebroadestpossiblefallbackintheexamplesforthischapter.Iexpectthereliabilityandconsistencyof
thesefeaturestoimproveastheHTML5implementationsstabilize.
Whenthebrowserisoffline,clickingtheNewsbuttontriggersarequestforaURLthatthebrowser
cannotservicefromtheapplicationcache,andthefallbackentryisusedinstead.Youcanseetheresult
inFigure5-5.TheURLinthebrowseraddressbarshowstheURLthatwasrequested,butthecontent
thatisshownisfromthefallbackresource.
Figure5-5.Usingthefallbackentry
125
www.it-ebooks.info
CHAPTER5CREATINGOFFLINEWEBAPPS
TheHTML5ApplicationCachespecificationprovidessupportformorecomplexfallbackentries,
includingper-URLfallbacksandtheuseofwildcards.However,asIwritethis,GoogleChromedoesn’t
supporttheseentries,andageneralfallback,suchasIhaveshowninthelisting,isallthatcanbereliably
used.
ThespecificationfortheHTML5ApplicationCachefeatureisambiguousaboutwhetherthe
browsershouldusetheregularcontentcachetosatisfyrequestsfornetworkentryresources.And,of
course,differentapproacheshavebeenadopted.GoogleChrometakesthemostliteralinterpretationof
thestandard.Whenthebrowserisoffline,networkentryresourcesarenotavailabletothewebapp.
MozillaFirefoxandOperatakeamoreforgivingapproach:iftheresourceisinthemainbrowsercache
whenthebrowsergoesoffline,itwillbeavailabletothewebapp.Ofcourse,thebrowsersareupdated
frequently,sotheremightbeadifferentsetofbehaviorsbythetimeyoureadthis.
CautionTheimplementationofthenetworkandfallbackfeaturescanbeinconsistent.Therearesomeoddities
intheimplementationsofthemainstreambrowsers,andasaconsequence,Itendtoavoidusingthesekindsof
entriesforcachedapplications.Theregularcacheentriesworkwell,however,andcanberelieduponinthose
browsersthatsupporttheapplicationcachefeature.
MonitoringOfflineStatus
HTML5definestheabilitytodeterminewhetherthebrowserisonline.Whatbeingofflinemeans
dependsontheplatformandthebrowser.Formobiledevices,beingofflineusuallyrequirestheuserto
switchtoairplanemodeortoexplicitlyswitchoffnetworkinginsomeotherway.Simplybeingoutof
coveragedoesn’tusuallychangethebrowserstatus.
Explicituseractionisrequiredformostdesktopbrowsersaswell.Forexample,FirefoxandOpera
bothhavemenuitemsthattogglethebrowserbetweenonlineandofflinemodes.Theexceptionis
GoogleChrome,whichmonitorstheunderlyingnetworkconnectionsandswitchestoofflineifno
networkdevicesareenabled.
NoteChromewillgointoofflinemodeonlywhenthereisnoenablednetworkconnection.Tocreatethe
screenshotinthissection,Ihadtodisablemymain(wireless)connection,manuallydisableanEthernetportthat
wasenabledbutnotpluggedintoanything,anddisableaconnectioncreatedbyavirtualmachinepackage.Only
thendidChromedecideitwastimetogooffline.Mostuserswon’thavethisproblem,butitissomethingtobear
inmind,especiallyifyouarenotgettingtheofflinebehavioryouexpect.
RecentversionsofthemainstreambrowsersimplementanHTML5featurethatreportsonwhether
thebrowserisonlineoroffline.Thisisusefulbothintermsofpresentingtheuserwithausefuland
contextualinterfaceandintermsofmanagingtheinternaloperationsofthewebapp.Todemonstrate
thisfeature,IamgoingtochangetheexamplewebappsothatthecachecontrolandNewsbuttonsare
displayedonlywhenthebrowserisonline.Listing5-8showsthechangestothescriptelement.
126
www.it-ebooks.info
CHAPTER5CREATINGOFFLINEWEBAPPS
Listing5-8.DetectingtheStateoftheNetwork
<script>
var cheeseModel = {
products: [
{category: "British Cheese", items : [
{id: "stilton", name: "Stilton", price: 9},
{id: "stinkingbishop", name: "Stinking Bishop", price: 17},
{id: "cheddar", name: "Cheddar", price: 17}]},
{category: "French Cheese", items: [
{id: "camembert", name: "Camembert", price: 18},
{id: "tomme", name: "Tomme de Savoie", price: 19},
{id: "morbier", name: "Morbier", price: 9}]},
{category: "Italian Cheese", items: [
{id: "gorgonzola", name: "Gorgonzola", price: 8},
{id: "fontina", name: "Fontina", price: 11},
{id: "parmesan", name: "Parmesan", price: 16}]}],
cache: {
status: ko.observable(window.applicationCache.status),
online: ko.observable(window.navigator.onLine)
}
};
$(document).ready(function() {
$('#buttonDiv input:submit').button();
$('div.navSelectors').buttonset();
enhanceViewModel();
ko.applyBindings(cheeseModel);
hasher.initialized.add(crossroads.parse, crossroads);
hasher.changed.add(crossroads.parse, crossroads);
hasher.init();
crossroads.addRoute("category/:cat:", function(cat) {
cheeseModel.selectedCategory(cat || cheeseModel.products[0].category);
});
$(window.applicationCache).bind("checking noupdate downloading " +
"progress cached updateready", function(e) {
cheeseModel.cache.status(window.applicationCache.status);
});
$(window).bind("online offline", function() {
cheeseModel.cache.online(window.navigator.onLine);
});
$('div.tagcontainer a').button().filter(':not([href])').click(function(e) {
e.preventDefault();
if ($(this).attr("data-action") == "update") {
127
www.it-ebooks.info
CHAPTER5CREATINGOFFLINEWEBAPPS
window.applicationCache.update();
} else {
window.applicationCache.swapCache();
window.location.reload(false);
}
});
});
</script>
Thewindowbrowserobjectsupportstheonlineandofflineeventsthataretriggeredwhenthe
browserstatuschanges.Youcangetthecurrentstatusthroughthewindow.navigator.onLineproperty,
whichreturnstrueifthebrowserisonlineandfalseifitisoffline.NotethattheLinonLineis
uppercase.Ihaveaddedanonlineobservabledataitemtotheviewmodel,whichIupdateinresponse
totheonlineandofflineevents.ThisisthesametechniquethatIusedfortheapplicationcachestatus,
anditallowsmetousetheviewmodeltopropagatechangesthroughtomymarkup.Listing5-9shows
thechangestotheHTMLelementsthatdisplaytheNewsandapplicationcachecontrolbuttons.
Listing5-9.AddingElementsandBindingstoRespondtotheBrowserOnlineStatus
<div id="logobar">
<img src="cheeselux.png">
<div class="tagcontainer">
<span id="tagline">Gourmet European Cheese</span>
<div>
<span data-bind="visible: cheeseModel.cache.online()">
<a data-bind="visible: cheeseModel.cache.status() != 4"
data-action="update" class="cachelink">Check for Updates</a>
<a data-bind="visible: cheeseModel.cache.status() == 4"
data-action="swapCache" class="cachelink">Apply Update</a>
<a class="cachelink" href="/news.html">News</a>
</span>
<span data-bind="visible: !cheeseModel.cache.online()">
(Offline)
</span>
</div>
</div>
</div>
Whenthebrowserisonline,thecachecontrolandtheNewsbuttonsaredisplayed.Whenthe
browserisoffline,Ireplacethebuttonswithasimpleplaceholder.YoucanseetheeffectinFigure5-6.
TipYouneedtoensurethatyouhavetherightversionoftheofflinecontentbeforetakingthebrowseroffline.
Beforerunningthisexample,youshouldeitherchangethemanifestorclearthebrowser’shistory.
128
www.it-ebooks.info
CHAPTER5CREATINGOFFLINEWEBAPPS
Figure5-6.Respondingtothebrowseronlinestatus
USING RECURRING AJAX REQUESTS POLYFILLS
ThereareJavaScriptpolyfilllibrariesavailablethatuseperiodicAjaxrequestsasasubstituteforthe
navigator.onLineproperty.Arequestforasmallfileismadetotheservereveryfewminutes,andifthe
requestfails,thebrowserisassumedtobeoffline.
Istronglyrecommendavoidingthisapproach.First,itisn’tresponsiveenoughtobeuseful.Ifyouaretrying
toworkoutwhenthebrowserisoffline,findingoutseveralminutesafterithappensisn’tmuchuse.During
theperiodsbetweentests,thestatusofthebrowserisunknownandcannotbereliedon.
Second,repeatedlyrequestingafileconsumesbandwidththatyouandtheuserhavetopayfor.Ifyou
haveapopularwebapp,thebandwidthcostsofperiodiccheckscanbesignificant.Moreimportantly,as
unlimiteddataplansformobiledevicesbecomelesscommon,assumingthatyoucanmakefreeuseof
yourusers’bandwidthisextremelypresumptuous.Myadviceistonotrelyonthissortofpolyfill.Justdo
withoutthenotificationsifthebrowserdoesn’tsupportthem.
UnderstandingwithAjaxandPOSTRequests
TheapplicationcachemakesitdifficulttoworkwithAjaxand,morebroadly,postingformsingeneral.
Andthingsgetworsewhenthebrowserisoffline,althoughperhapsnotinthewayyoumightexpect.In
thissection,I’llshowyoutheproblemsandthelimitedoptionsthatareavailabletodealwiththem.
First,however,IneedtoupdatetheCheeseLuxwebappsothatitdependsonanAjaxGETrequestto
operate.Listing5-10showstherequiredchangedtothescriptelement(nochangesareneededtothe
markupforthisexample).
129
www.it-ebooks.info
CHAPTER5CREATINGOFFLINEWEBAPPS
Listing5-10.AddinganAjaxGETRequestRequest
...
<script>
var cheeseModel = {
cache: {
status: ko.observable(window.applicationCache.status),
online: ko.observable(window.navigator.onLine)
}
};
$.getJSON("products.json", function(data) {
cheeseModel.products = data;
}).success(function() {
$(document).ready(function() {
$('#buttonDiv input:submit').button();
$('div.navSelectors').buttonset();
enhanceViewModel();
ko.applyBindings(cheeseModel);
hasher.initialized.add(crossroads.parse, crossroads);
hasher.changed.add(crossroads.parse, crossroads);
hasher.init();
crossroads.addRoute("category/:cat:", function(cat) {
cheeseModel.selectedCategory(cat || cheeseModel.products[0].category);
});
$(window.applicationCache).bind("checking noupdate downloading " +
"progress cached updateready", function(e) {
cheeseModel.cache.status(window.applicationCache.status);
});
$(window).bind("online offline", function() {
cheeseModel.cache.online(window.navigator.onLine);
});
});
});
</script>
...
$('div.tagcontainer a').button().filter(':not([href])').click(function(e) {
e.preventDefault();
if ($(this).attr("data-action") == "update") {
window.applicationCache.update();
} else {
window.applicationCache.swapCache();
window.location.reload(false);
}
});
130
www.it-ebooks.info
CHAPTER5CREATINGOFFLINEWEBAPPS
Inthislisting,IhaveusedthejQuerygetJSONmethod.Thisisaconveniencemethodthatmakesan
AjaxGETrequestfortheJSONfilespecifiedbythefirstmethodargument,whichisproducts.jsoninthis
case.WhentheAjaxrequestshascompleted,jQueryparsestheJSONdatatocreateaJavaScriptobject,
whichispassedtothefunctionspecifiedbythesecondmethodargument.Inmylisting,thefunction
simplytakestheJavaScriptobjectandassignsittotheproductspropertyoftheviewmodel.The
products.jsonfilecontainsasupersetofthedataIhavebeendefininginline.Thesamecategories,
products,andpricesaredefined,alongwithanadditionaldescriptionofeachcheese.Listing5-11shows
anextractfromproducts.json.
Listing5-11.AnExtractfromtheproducts.jsonFile
...
{"id": "stilton", "name": "Stilton", "price": 9,
"description": "A semi-soft blue cow's milk cheese produced in the Nottinghamshire region. A
strong cheese with a distinctive smell and taste and crumbly texture."},
...
InthelistingIchainthegetJSONmethodwithacalltosuccess.Thesuccessmethodispartofthe
jQuerysupportforJavaScriptPromises,whichmakeiteasytouseandmanageasynchronousoperations
likeAjaxrequests.Thefunctionpassedtothesuccessmethodwon’tbeexecuteduntilthegetJSON
methodhascompleted,ensuringthatmyviewmodeliscompletebeforetherestofmyscriptisrun.
ThisapproachtogettingcoredatafromJSONisacommonone,especiallywherethedataissourced
fromadifferentsetofsystemstotherestofthewebapp.And,ifusedcarefully,itcanensurethatthe
userhasthemostrecentdatabutstillhasthebenefitofacachedapplication.
UnderstandingtheDefaultAjaxGETBehavior
ThebrowsertreatsanAjaxGETrequestinaverysimpleway.TherequestwillfailiftheAjaxrequestisfor
aresourcethatisnotinthemanifest,evenwhenthebrowserisonline.
Formyexampleapplication,thismeansthatdataisreturnedfromtherequestanditdiesahorrible
death.ThefunctionIpassedasanargumenttothegetJSONmethodisexecutedonlyiftheAjaxrequest
succeeds,andthesameistrueforthefunctionpassedtothesuccessmethod.Becauseneitherfunction
isexecuted,themainpartofmyscriptcodeisn’tperformed,andIleavetheuserstranded.Worse,since
theapplicationcachecontrolbuttonsareneversetup,Idon’tgivetheuserameanstoupdatethe
applicationtofixtheproblem.
Ihaveshownthisscenariobecauseitisverycommonlyencounteredwhenprogrammersfirststart
usingtheapplicationcache.I’llshowyouhowtomaketheAjaxconnectionworkshortly,butfirst,there
areacoupleofimportantchangestobemade.
RestructuringtheApplication
Thefirstchangeistostructuretheapplicationsothatthecorebehaviorthatwillgettheuserbackoutof
troublewillalwaysbeexecuted.Myinitiallistingisjusttoooptimistic,andIneedtoseparatethoseparts
ofthecodethatshouldalwaysberun.Therearelotsofdifferenttechniquesfordoingthis,butIfindthe
simplestisjusttocreateanotherfunctionthatiscontingentonthejQueryreadyevent.Listing5-12
showsthechangesIrequiretothescriptelement.
131
www.it-ebooks.info
CHAPTER5CREATINGOFFLINEWEBAPPS
Listing5-12.RestructuringthescriptElement
...
<script>
var cheeseModel = {
cache: {
status: ko.observable(window.applicationCache.status),
online: ko.observable(window.navigator.onLine)
}
};
$.getJSON("products.json", function(data) {
cheeseModel.products = data;
}).success(function() {
$(document).ready(function() {
enhanceViewModel();
ko.applyBindings(cheeseModel);
hasher.initialized.add(crossroads.parse, crossroads);
hasher.changed.add(crossroads.parse, crossroads);
hasher.init();
crossroads.addRoute("category/:cat:", function(cat) {
cheeseModel.selectedCategory(cat || cheeseModel.products[0].category);
});
});
}).complete(function() {
$(document).ready(function() {
$('#buttonDiv input:submit').button();
$('div.navSelectors').buttonset();
$(window).bind("online offline", function() {
cheeseModel.cache.online(window.navigator.onLine);
});
$(window.applicationCache).bind("checking noupdate downloading " +
"progress cached updateready", function(e) {
cheeseModel.cache.status(window.applicationCache.status);
});
});
});
$('div.tagcontainer a').button().filter(':not([href])').click(function(e) {
e.preventDefault();
if ($(this).attr("data-action") == "update") {
window.applicationCache.update();
} else {
window.applicationCache.swapCache();
window.location.reload(false);
}
});
132
www.it-ebooks.info
CHAPTER5CREATINGOFFLINEWEBAPPS
</script>
...
Ihavepulledallofthecodethatisn’tcontingentonasuccessfulAjaxrequesttogetherandplacedit
inafunctionpassedtothecompletemethod,whichIaddtothechainofmethodcalls.Thisfunctionwill
beexecutedwhentheAjaxrequestfinishes,irrespectiveofwhetheritsucceededorfailed.
Now,evenwhentheAjaxrequestfails,thecontrolsforupdatingthecacheandapplyingchangesare
alwaysavailable.GiventhatAjaxproblemsarethemostlikelyreasonforerrorsattheclient,givingthe
userawaytoapplyanupdateisessential.Otherwise,youaregoingtohavetoprovideper-browser
instructionsforclearingthecache.Itisnotaperfectsolution,becauseIamunabletoapplymydata
bindings,soelementsthatIwouldratherwerehiddenarevisible.IcouldusetheCSSdisplayproperty
tohidesomeoftheseitems,butIthinkjustgivingtheusertheabilitytodownloadandapplyanupdate
iswhatisessential.YoucanseetheeffectbeforeandaftertherestructuringinFigure5-7.
Figure5-7.Theeffectofrestructuringtheapplication
HandlingtheAjaxError
TheotherchangeIneedtomakeistoaddsomekindoferrorhandlerforwhentheAjaxrequestfails.
Thismayseemlikeabasictechnique,butmanywebapplicationsarecodedonlyforsuccess,andwhen
theconnectionfails,everythingfallsapart.TherearelotsofwaysofhandlingAjaxerrors,buttheone
showninListing5-13usessomejQueryfeatures.
Listing5-13.AddingSupportforHandlingAjaxErrors
<script>
var cheeseModel = {
cache: {
status: ko.observable(window.applicationCache.status),
online: ko.observable(window.navigator.onLine)
}
};
$.getJSON("products.json", function(data) {
cheeseModel.products = data;
}).success(function() {
$(document).ready(function() {
enhanceViewModel();
ko.applyBindings(cheeseModel);
133
www.it-ebooks.info
CHAPTER5CREATINGOFFLINEWEBAPPS
hasher.initialized.add(crossroads.parse, crossroads);
hasher.changed.add(crossroads.parse, crossroads);
hasher.init();
crossroads.addRoute("category/:cat:", function(cat) {
cheeseModel.selectedCategory(cat || cheeseModel.products[0].category);
});
});
}).error(function() {
var dialogHTML = '<div>Try again later</div>';
$(dialogHTML).dialog({
modal: true,
title: "Ajax Error",
buttons: [{text: "OK", click: function() {$(this).dialog("close")}}]
});
}).complete(function() {
$(document).ready(function() {
$('#buttonDiv input:submit').button();
$('div.navSelectors').buttonset();
$(window).bind("online offline", function() {
cheeseModel.cache.online(window.navigator.onLine);
});
$(window.applicationCache).bind("checking noupdate downloading " +
"progress cached updateready", function(e) {
cheeseModel.cache.status(window.applicationCache.status);
});
});
});
</script>
$('div.tagcontainer a').button().filter(':not([href])').click(function(e) {
e.preventDefault();
if ($(this).attr("data-action") == "update") {
window.applicationCache.update();
} else {
window.applicationCache.swapCache();
window.location.reload(false);
}
});
jQuerymakesiteasytohandleerrorswiththeerrormethod.ThisisanotherpartofthePromises
feature,andthefunctionpassedtotheerrormethodwillbeexecutedifthereisaproblemwiththe
request.Inthisexample,IcreatedasimplejQueryUIdialogboxthattellstheuserthatthereisa
problem.
134
www.it-ebooks.info
CHAPTER5CREATINGOFFLINEWEBAPPS
AddingtheAjaxURLtotheMainManifestorFALLBACKSections
TheworstthingyoucandoatthispointisaddtheAjaxURLtothemainsectionofthemanifest.The
browserwilltreattheURLlikeanyotherresource,downloadingandcachingthecontentwhenthe
manifestisprocessed.WhentheclientmakestheAjaxrequest,thebrowserwillreturnthecontentfrom
theapplicationcache,andthedatawon’tbeupdateduntilamanifestchangetriggersacacheupdate.
Theresultofthisisthatyouruserswillbeworkingwithstaledata,whichisgenerallycontrarytothe
reasoningbehindmakingtheAjaxrequestinthefirstplace.
YougetprettymuchthesameresultifyouaddtheURLtotheFALLBACKsection.Everyrequest,even
whenthebrowserisonline,willbesatisfiedbywhateveryousetasthefallback,andnorequestwillever
bemadetotheserver.
AddingtheAjaxURLtotheManifestNETWORKSection
Thebestapproach(albeitfarfromideal)istoaddtheAjaxURLtotheNETWORKsectionofthemanifest.
Whenthebrowserisonline,theAjaxrequestswillbepassedtotheserver,andthelatestdatawillbe
presentedtotheuser.
Theproblemsstartwhenthebrowserisoffline.TherearetwodifferentapproachestohandlingAjax
requestsinanofflinebrowser.Thefirstapproach,whichyoucanseeinGoogleChrome,isthattheAjax
requestwillfail.YourAjaxerrorhandlerwillbeinvoked,andthereisacleanfailure.
TheotherapproachcanbeseeninFirefox.Whenthebrowserisoffline,Ajaxrequestswillbe
servicedusingthemainbrowsercacheifpossible.Thiscreatestheoddsituationwheretheuserwillget
staledataifarequestforthesameURLwasmadebeforethebrowserwentofflineandwillgetanerrorif
thisisthefirsttimethattheURLhasbeenaskedfor.
UnderstandingthePOSTRequestBehavior
ThewaythatPOSTrequestsarehandledisalotmoreconsistentthanforGETrequests.Ifthebrowseris
online,thenthePOSTrequestwillbemadetotheserver.Ifthebrowserisoffline,thentherequestwill
fail.ThisistrueforPOSTrequeststhataremadeusingregularHTMLandforPOSTrequestsmadeusing
Ajax.
ThisleadstoannoyedusersbecausePOSTingaformusuallycomesaftersomeperiodofactivityon
theirpart.InthecaseoftheCheeseLuxexample,theuserwillhavepagedthroughthecategoriesand
enteredtheamountsofeachproducttheyrequire.Whentheycometosubmittheirorder,thebrowser
willshowanerrorpage.Youcan’tevenusetheFALLBACKsectionofthemanifesttonominateapagetobe
showninsteadoftheerror.
Theonlysensiblethingtodoistointercepttheformsubmissionandusethenavigator.onLine
propertyandeventstomonitorthebrowserstatusandpreventtheuserfromtryingtopostcontent
whenthebrowserisoffline.InChapter6,I’llshowyousometechniquesforpreservingtheresultofthe
user’seffort,readyforwhenthebrowsercomesbackonline.
135
www.it-ebooks.info
CHAPTER5CREATINGOFFLINEWEBAPPS
Summary
Inthischapter,IshowedyouhowtousetheHTML5ApplicationCachetocreateofflineapplications.By
usingtheapplicationcache,youcancreateapplicationsthatareavailableevenwhentheuserdoesn’t
haveanetworkconnection.Althoughthecoreoftheapplicationcacheiswell-supported,therearesome
anomalies,andcarefuldesignandtestingarerequiredtogetaresultthatisreliableandrobust.Inthe
nextchapter,I’llshowyouhowtousesomerelatedfunctionalitythathelpssmoothoutsomeofthe
roughedgesofofflineappsandthatcanbeusedtocreateabetterexperiencefortheuser.
136
www.it-ebooks.info
CHAPTER 6
Storing Data in the Browser
Anaturalcomplementtoofflineapplicationsisclient-sidedatastorage.HTML5definessomeuseful
JavaScriptAPIsforstoringdatainthebrowser,rangingfromsimplename/valuepairstousinga
JavaScriptobjectdatabase.Inthischapter,Ishowyouhowtobuildapplicationsthatrelyonpersistently
storeddata,includingdetailsofhowtousesuchdatainanofflinewebapplication.
CautionThebrowsersupportfordatastorageismixed.Youshouldruntheexamplesinthischapterusing
GoogleChrome,withtheexceptionofthoseintheIndexedDBsection,whichwillrunonlyinMozillaFirefox.
UsingLocalStorage
ThesimplestwaytostoredatainthebrowseristousetheHTML5localstoragefeature.Thisallowsyou
tostoresimplename/valuepairsandretrieveormodifythemlater.Thedataisstoredpersistentlybutis
notguaranteedtobestoredforever.Thebrowserisfreetodeleteyourdataifitneedsthespace(orifthe
datahasn’tbeenaccessedforalongtime),and,ofcourse,theusercanclearthedatastoreatanytime,
evenwhenyourwebappisrunning.Theresultisdatathatisbroadly,butnotindefinitely,persistent.
UsinglocalstorageisverysimilartousingaregularJavaScriptarray,asListing6-1demonstrates.
Listing6-1.UsingLocalStorage
<!DOCTYPE html>
<html>
<head>
<title>Local Storage Example</title>
<link rel="stylesheet" type="text/css" href="jquery-ui-1.8.16.custom.css"/>
<link rel="stylesheet" type="text/css" href="styles.css"/>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script src="jquery-ui-1.8.16.custom.js" type="text/javascript"></script>
<script src='knockout-2.0.0.js' type='text/javascript'></script>
<script src='utils.js' type='text/javascript'></script>
<script src='signals.js' type='text/javascript'></script>
<script src='crossroads.js' type='text/javascript'></script>
<script src='hasher.js' type='text/javascript'></script>
<script>
var viewModel = {
137
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
items: ["Apple", "Orange", "Banana"],
selectedItem: ko.observable("Apple")
};
$(document).ready(function() {
ko.applyBindings(viewModel);
$('div.catSelectors').buttonset();
hasher.initialized.add(crossroads.parse, crossroads);
hasher.changed.add(crossroads.parse, crossroads);
hasher.init();
crossroads.addRoute("select/{item}", function(item) {
viewModel.selectedItem(item);
localStorage["selection"] = item;
});
viewModel.selectedItem(localStorage["selection"] || viewModel.items[0]);
});
</script>
</head>
<body>
<div class="catSelectors" data-bind="foreach: items">
<a data-bind="formatAttr: {attr: 'href', prefix: '#select/', value: $data},
css: {selectedItem: ($data == viewModel.selectedItem())}">
<span data-bind="text: $data"></span>
</a>
</div>
<div data-bind="foreach: items">
<div class="item" data-bind="fadeVisible: $data == viewModel.selectedItem()">
The selected item is: <span data-bind="text: $data"></span>
</div>
</div>
</body>
</html>
Todemonstratelocalstorage,IhaveusedthesimpleexamplefromChapter4,whichallowsmeto
focusonthestoragetechniqueswithoutthefeaturesfromotherchaptersgettingintheway.Asthe
listingshows,gettingstartedwithlocalstorageisprettysimple.ThegloballocalStorageobjectactslike
anarray.Whentheusermakesaselectioninthissimplewebapp,Istoretheselecteditemusingarraystylenotation,likethis:
localStorage["selection"] = item;
TipKeysarecase-sensitive(sothatselectionandSelectionwouldrepresentdifferentdataitems),and
assigningavaluetoakeythatalreadyexistsoverwritesthepreviouslydefinedvalue.
138
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
Thisstatementcreatesanewlocalstorageitem,whichIcanreadbackusingthesamearray-style
notation,likethis:
viewModel.selectedItem(localStorage["selection"] || viewModel.items[0]);
Theeffectofaddingthesetwostatementstotheexampleistocreatesimplepersistenceforthe
user’sselection.Whenthewebappisloaded,Ichecktoseewhetherthereisdatastoredunderthe
selectionkeyand,ifthereis,setthecorrespondingdataitemintheviewmodel,whichrestoresthe
user’sselectionfromanearliersession.
TipItisimportantnottouselocalstorageforsensitiveinformationortotrusttheintegrityofdataretrieved
fromlocalstorageforcriticalfunctionsinyourwebapp.Userscanseeandeditthecontentsoflocalstorage,
whichmeansthatnothingyoustoreissecretandeverythingcanbechanged.Don’tstoreanythingyoudon’twant
publicallydisseminated,anddon’trelyonlocalstoragetogiveprivilegedaccesstoyourwebapp.
Fromthatpointon,Iupdatethevalueassociatedwiththeselectionkeyeachtimemyrouteis
matchedbyaURLchange.Iincludedafallbacktoadefaultselectiontocopewiththepossibilitythatthe
localstoragedatahasbeendeleted(orthisisthefirsttimethattheuserhasloadedthewebapp).Totest
thisfeature,loadtheexamplewebapp,selectoneoftheoptions,andthenreloadthewebpage.The
browserwillreloadthedocument,executetheJavaScriptcodeafresh,andrestoreyourselection.
StoringJSONData
Thespecificationforlocalstoragerequiresthatkeysandvaluesarestrings,justlikeintheprevious
example.Beingabletostorealistofname/valuepairsisn’talwaysthatuseful,butwecanbuildonthe
supportforstringstouselocalstorageforJSONdata,asshowninListing6-2.
Listing6-2.UsingLocalStorageforJSONData
...
<script>
var viewModel = {
selectedItem: ko.observable()
};
function loadViewModelData() {
var storedData = localStorage["viewModelData"];
if (storedData) {
var storedDataObject = JSON.parse(storedData);
viewModel.items = storedDataObject.items;
viewModel.selectedItem(storedDataObject.selectedItem);
} else {
viewModel.items = ["Apple", "Orange", "Banana"];
viewModel.selectedItem("Apple");
}
}
139
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
function storeViewModelData() {
var viewModelData = {
items: viewModel.items,
selectedItem: viewModel.selectedItem()
};
localStorage["viewModelData"] = JSON.stringify(viewModelData);
}
$(document).ready(function() {
loadViewModelData();
ko.applyBindings(viewModel);
$('div.catSelectors').buttonset();
hasher.initialized.add(crossroads.parse, crossroads);
hasher.changed.add(crossroads.parse, crossroads);
hasher.init();
crossroads.addRoute("select/{item}", function(item) {
viewModel.selectedItem(item);
storeViewModelData();
});
});
</script>
...
IhavedefinedtwonewfunctionsinthescriptelementtosupportstoringJSON.The
storeViewModelDatafunctioniscalledwhenevertheusermakesaselection.JSONisonlyabletostore
datavaluesandnotJavaScriptfunctions,soIextractthedatavaluesfromtheviewmodelandusethem
tocreateanewobject.IpassthisobjecttotheJSON.stringifymethod,whichreturnsaJSONstring,like
this:
{"items":["Apple","Orange","Banana"],
"selectedItem":"Banana"}
IstorethisstringbyassociatingitwiththeviewModelDatakeyinlocalstorage.Thecorresponding
functionisloadViewModelData.IcallthisfunctionwhenthejQueryreadyeventisfiredanduseitto
completetheviewmodel.
TipThepersistentnatureoflocalstoragemeansthatifyoureuseakeytostoreadifferentkindofdata,you
runtheriskofencounteringtheoldformatthatwasstoredinaprevioussession.Thesimplestwaytohandlethis
indevelopmentistoclearthebrowser’scache.Inproduction,youmustbeabletodetecttheolddataandeither
processitor,attheveryleast,beabletodiscarditwithoutgeneratinganyerrors.
140
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
IloadtheJSONstringandusetheJSON.parsemethodtocreateaJavaScriptobjectifthereislocal
storagedataassociatedwiththeviewModelDatakey.Icanthenreadthepropertiesoftheobjectto
populatetheviewmodel.Ofcourse,Icannotrelyontherebeingdataavailable,soIfallbacktosome
sensibledefaultvaluesifneeded.
STORING OBJECT DATA
Itwasn’thardtoseparatethedatafromtheobjectthatcontaineditinmysimpleexample,butitcanbe
significantlymoredifficultinacomplexwebapplication.Youmightbetemptedtoshortcutthisprocessby
storingobjectsdirectly,ratherthanmappingdatatostrings.Don’tdothis;itwillonlycauseyouproblems.
Hereisacodesnippetthatshowslocalstoragebeingusedwithobjects:
...
<script>
var viewModel = {};
function loadViewModelData() {
var storedData = localStorage["viewModelData"];
if (storedData) {
viewModel = storedData;
} else {
viewModel.items = ["Apple", "Orange", "Banana"];
viewModel.selectedItem = ko.observable("Apple");
}
}
function storeViewModelData() {
localStorage["viewModelData"] = viewModel;
}
$(document).ready(function() {
loadViewModelData();
ko.applyBindings(viewModel);
$('div.catSelectors').buttonset();
hasher.initialized.add(crossroads.parse, crossroads);
hasher.changed.add(crossroads.parse, crossroads);
hasher.init();
crossroads.addRoute("select/{item}", function(item) {
viewModel.selectedItem(item);
storeViewModelData();
});
});
</script>
...
141
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
Thistechniquedoesn’twork.Thebrowserwon’tcomplainwhenyoustoreobjects,andifyoureadthe
valuebackwithinthesamesession,everythinglooksfine.Butthebrowserserializestheobjectinorderto
storeitforfuturesessions.FormostJavaScriptobjects,thestoredvaluewillbe[object Object],which
istheresultyougetifyoucallthetoStringmethod.Whentheuserrevisitsthewebapp,thevalueinlocal
storageisn’tavalidJavaScriptobjectandcan’tbeparsed.Thisisthekindofproblemthatshouldbe
detectedduringtesting,butIseethisissuealot,notleastbecauseevenprojectsthattaketesting
seriouslydon’tgenerallyrevisittheapplicationformultiplesessions.
StoringFormData
Localstorageisideallysuitedformakingformdatapersistent.Thekey/valuemappingsuitsthenature
offormelementsverywell,andwithverylittleeffort,youcancreateformsthatarepersistentbetween
sessions,asListing6-3shows.
Listing6-3.UsingLocalStoragetoCreatePersistentForms
<!DOCTYPE html>
<html>
<head>
<title>Local Storage Example</title>
<link rel="stylesheet" type="text/css" href="jquery-ui-1.8.16.custom.css"/>
<link rel="stylesheet" type="text/css" href="styles.css"/>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script src="jquery-ui-1.8.16.custom.js" type="text/javascript"></script>
<script src='knockout-2.0.0.js' type='text/javascript'></script>
<script>
var viewModel = {
personalDetails: [
{name: "name", label: "Name", value: ko.observable()},
{name: "city", label: "City", value: ko.observable()},
{name: "country", label: "Country", value: ko.observable()}
]
};
$(document).ready(function() {
$.each(viewModel.personalDetails, function(index, item) {
item.value(localStorage[item.name] || "");
item.value.subscribe(function(newValue) {
localStorage[item.name] = newValue;
});
});
ko.applyBindings(viewModel);
$('#buttonDiv input').button().click(function(e) {
localStorage.clear();
});
});
</script>
</head>
142
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
<body>
<form action="/formecho" method="POST">
<div class="cheesegroup">
<div class="grouptitle">Your Details</div>
<div class="groupcontent centered">
<div data-bind="foreach: personalDetails">
<span data-bind="text: label"></span>:
<input class="stwin" data-bind="attr: {name: name}, value: value">
</div>
</div>
</div>
<div id="buttonDiv">
<input type="submit" value="Submit">
<input type="reset" value="Reset">
</div>
</form>
</body>
</html>
Ihavedefinedasimplethree-fieldformelementinthisexample,whichyoucanseeinFigure6-1.
Theformcapturestheuser’sname,city,andcountryandispostedtothe/formechoURLattheserver,
whichsimplyrespondswithdetailsofthedatathatwassubmitted.
Figure6-1.Usinglocalstoragewithformelements
Ihaveusedaviewmodelasanintermediarybetweentheinputelementsandlocalstorage.When
theuserentersavalueintooneoftheinputelements,thevaluedatabindingupdatesthecorresponding
observabledataitemintheviewmodel.Iusethesubscribefunctiontoreceivenotificationsofthese
changesandwritetheupdatetolocalstorage,likethis:
$.each(viewModel.personalDetails, function(index, item) {
item.value(localStorage[item.name] || "");
item.value.subscribe(function(newValue) {
localStorage[item.name] = newValue;
});
});
143
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
Isetupthesubscriptionbyenumeratingthroughtheitemsintheviewmodel.Iusethisopportunity
tosettheinitialvaluesintheviewmodelfromlocalstorageifthereisdataavailable,likethis:
item.value(localStorage[item.name] || "");
WhenIsettheinitialvalue,thevaluesfromlocalstoragearepropagatedthroughtheviewmodelto
theinputelements,keepingeverythingup-to-date.
Itdoesn’tmakesensetocontinuetostoretheformdataoncetheformhasbeensubmittedorwhen
theuserclickstheResetbutton.WheneithertheSubmitorResetbuttonisclicked,Iremovethedata
fromlocalstorage,likethis:
$('#buttonDiv input').button().click(function(e) {
localStorage.clear();
});
Theclearmethodremovesallofthedatainlocalstorageforthewebapp(butnotforotherweb
apps;onlytheuserorthebrowseritselfcanaffectstorageacrosswebapps).Ididnotpreventthedefault
actionforeitherbutton,whichmeansthattheformwillbesubmittedbythesubmitbutton,andthe
formwillberesetbytheresetbutton.
TipStrictlyspeaking,Ineednothavehandledtheclickeventfortheresetbuttonsincetheviewmodelwould
haveledtoemptyvaluesbeingwrittentolocalstorage.Insituationslikethese,Itendtoprefercleansingthedata
twiceinordertogetsimplerJavaScriptcode.
Theeffectofthislittlewebappisthattheformdataispersistentuntiltheusersubmitstheform.If
theusernavigatesawayfromtheformbeforesubmittingit,thedatatheyenteredbeforenavigatingaway
willberestoredthenexttimethewebappisloaded.
SynchronizingViewModelDataBetweenDocuments
Thedatainlocalstorageisstoredonaper-originbasis,meaningthateachoriginhasitsownseparate
localstoragearea.Thismeansyoudon’thavetoworryaboutkeycollisionwithotherpeople’sweb
applications.Italsomeansthatwecanusewebstoragetosynchronizeviewmodelsbetweendifferent
documentswithinthesamedomain.
Whenusinglocalstorageinthisway,Iwanttobenotifiedwhenanotherdocumentmodifiesa
storeddatavalue.Icanreceivesuchnotificationsbyhandlingthestorageevent,whichisemittedbythe
windowbrowserobject.Tomakethiseventeasiertouse,Ihavecreatedanewkindofobservabledata
itemthatautomaticallypersistsitselftolocalstorageandthatloadschangedvaluesinresponsetothe
storageevent.Iaddedthisnewfunctionalitytotheutils.jsfile,asshowninListing6-4.
Listing6-4.CreatingaPersistentObservableDataItem
...
ko.persistentObservable = function(keyName, initialValue) {
var obItem = ko.observable(localStorage[keyName] || initialValue);
$(window).bind("storage", function(e) {
144
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
if (e.originalEvent.key == keyName) {
obItem(e.originalEvent.newValue);
}
});
obItem.subscribe(function(newValue) {
localStorage[keyName] = newValue;
});
return obItem;
}
...
Thiscodeisawrapperaroundthestandardobservabledataitem,thelocalstoragedataarray,and
thestorageevent.Thefunctioniscalledwithakeynamethatreferstoadataiteminlocalstorage.When
thefunctioniscalled,Iusethekeytocheckwhetherthereisalreadydatainlocalstorageforthe
specifiedkeyand,ifthereis,settheinitialvalueoftheobservable.Ifthereisn’tadefaultvalue,Iusethe
initialValuefunctionargument:
var obItem = ko.observable(localStorage[keyName] || initialValue);
IusejQuerytobindtothestorageeventonthewindowobject.jQuerynormalizesevents,wrapping
theeventobjectsemittedbyelementswithajQuery-specificsubstitute.Ineedtogettotheunderlying
eventobjectbecauseitcontainsinformationaboutthechangeinlocalstorage;Idothisthroughthe
originalEventproperty.Whenhandlingthestorageevent,theoriginalEventpropertyreturnsa
StorageEventobject,themostusefulpropertiesofwhicharedescribedinTable6-1.
Table6-1.PropertiesoftheStorageEventObject
Property
Description
key
Returnsthekeyfortheitemthathasbeenmodified
oldValue
Returnstheoldvaluefortheitemthathasbeenmodified
newValue
Returnsthenewvaluefortheitemthathasbeenmodified
url
ReturnstheURLofthedocumentthatmadethechange
Intheexample,IusethekeypropertytodeterminewhetherthisisaneventforthedataitemthatI
ammonitoringand,ifitis,thenewValuepropertytoupdatetheregularobservabledataitem:
$(window).bind("storage", function(e) {
if (e.originalEvent.key == keyName) {
obItem(e.originalEvent.newValue);
}
});
Finally,IusetheKOsubscribemethodsothatIcanupdatethelocalstoragevalueinresponseto
changesintheviewmodel:
obItem.subscribe(function(newValue) {
localStorage[keyName] = newValue;
});
145
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
Withjustafewlinesofcode,Ihavebeenabletocreateapersistentobservabledataitemformyview
model.
Ihavenothadtotakeanyspecialprecautionstopreventaninfiniteloopofevent-updatesubscription-eventoccurring.Therearetworeasonsforthis.First,theKOobservabledataitemthatmy
codewrapsaroundissmartenoughtoissueupdatesonlywhenanupdatedvalueisdifferentfromthe
existingvalue.
Second,thebrowsertriggersthestorageeventonlyinotherdocumentsinthesameoriginandnot
thedocumentinwhichthechangewasmade.Ihavealwaysthoughtthiswasslightlyodd,butitdoes
meanthatmycodeissimplerthanitwouldotherwisehavebeen.
Todemonstratemynewlypersistentdataitems,Ihavedefinedanewdocumentcalled
embedded.html,thecontentofwhichisshowninListing6-5.
Listing6-5.ANewDocumentThatUsesPersistentObservableDataItems
<!DOCTYPE html>
<html>
<head>
<title>Embedded Storage Example</title>
<link rel="stylesheet" type="text/css" href="jquery-ui-1.8.16.custom.css"/>
<link rel="stylesheet" type="text/css" href="styles.css"/>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script src="jquery-ui-1.8.16.custom.js" type="text/javascript"></script>
<script src='knockout-2.0.0.js' type='text/javascript'></script>
<script src='utils.js' type='text/javascript'></script>
<script>
var viewModel = {
personalDetails: [
{name: "name", label: "Name", value: ko.persistentObservable("name")},
{name: "city", label: "City", value: ko.persistentObservable("city")},
{name: "country", label: "Country",
value: ko.persistentObservable("country")}
]
};
$(document).ready(function() {
ko.applyBindings(viewModel);
});
</script>
</head>
<body>
<div class="cheesegroup">
<div class="grouptitle">Embedded Document</div>
<div class="groupcontent centered">
<div data-bind="foreach: personalDetails">
<span data-bind="text: label"></span>:
<input class="stwin" data-bind="attr: {name: name}, value: value">
</div>
</div>
</div>
</body>
</html>
146
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
Thisdocumentduplicatestheinputelementsfromthemainexample,butwithouttheformand
buttonelements.Itdoes,however,haveaviewmodelthatusesthepersistentObservabledataitem,
meaningthatchangestotheinputelementvaluesinthisdocumentwillbereflectedinlocalstorageand,
equally,thatchangesinlocalstoragewillbereflectedintheinputelements.Ihavenotsupplieddefault
valuesforthepersistentobservableitems;ifthereisnolocalstoragevalue,thenIwanttheinitialvalue
todefaulttonull,whichIachievebynotsupplyingasecondargumenttothepersistentObservable
function.
Allthatremainsistomodifythemaindocument.Forsimplicity,Iamembeddingonedocument
insideanother,butlocalstorageissharedacrossanydocumentsfromthesameorigin,meaningthatthis
techniquewillworkwhenthosedocumentsarewithindifferentbrowsertabsorwindows.Listing6-6
showsthemodificationstoexample.html,includingembeddingtheembedded.htmldocument.
Listing6-6.ModifyingtheMainExampleDocument
<!DOCTYPE html>
<html>
<head>
<title>Local Storage Example</title>
<link rel="stylesheet" type="text/css" href="jquery-ui-1.8.16.custom.css"/>
<link rel="stylesheet" type="text/css" href="styles.css"/>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script src="jquery-ui-1.8.16.custom.js" type="text/javascript"></script>
<script src='knockout-2.0.0.js' type='text/javascript'></script>
<script src='utils.js' type='text/javascript'></script>
<script>
var viewModel = {
personalDetails: [
{name: "name", label: "Name", value: ko.persistentObservable("name")},
{name: "city", label: "City", value: ko.persistentObservable("city")},
{name: "country", label: "Country",
value: ko.persistentObservable("country")}
]
};
$(document).ready(function() {
ko.applyBindings(viewModel);
$('#buttonDiv input').button().click(function(e) {
localStorage.clear();
});
});
</script>
</head>
<body>
<form action="/formecho" method="POST">
<div class="cheesegroup">
<div class="grouptitle">Your Details</div>
<div class="groupcontent centered">
<div data-bind="foreach: personalDetails">
<span data-bind="text: label"></span>:
147
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
<input class="stwin" data-bind="attr: {name: name}, value: value">
</div>
</div>
</div>
<iframe src="embedded.html"></iframe>
<div id="buttonDiv">
<input type="submit" value="Submit">
<input type="reset" value="Reset">
</div>
</form>
</body>
</html>
IhaveusedthesamekeysforthepersistentObservablefunctionwhendefiningtheviewmodeland
addedaniframeelementthatembedstheotherHTMLdocument.Sincebothareloadedfromthesame
origin,thebrowsersharesthesamelocalstoragebetweenthem.Changingthevalueofaninputelement
inonedocumentwilltriggeracorrespondingchangeintheotherdocument,vialocalstorageandthe
twoviewmodels.
CautionThebrowsersdon’tprovideanyguaranteesabouttheintegrityofadataitemifupdatesarewrittento
localstoragefromtwodocumentssimultaneously.Itishardtocaterforthiseventuality(andIhaveneverseenit
happen),butitisprudenttoassumethatdatacorruptioncanoccurifyouaresharinglocalstorage.
UsingSessionStorage
Thecomplementtolocalstorageissessionstorage,whichisaccessedthroughthesessionStorageobject.
ThesessionStorageandlocalStorageobjectsareusedinthesamewayandemitthesamestorage
event.Thedifferenceisthatthedataisdeletedwhenthedocumentisclosedinthebrowser(more
specifically,thedataisdeletedwhenthetop-levelbrowsingcontextisdestroyed,butthat’susuallythe
samething).
Themostcommonuseforsessionstorageistopreservedatawhenadocumentisreloaded.Thisisa
usefultechnique,althoughIhavetoadmitthatItendtouselocalstoragetoachievethesameeffect
instead.Themainbenefitofsessionstorageisperformance,sincethedataisusuallyheldinmemory
anddoesn’tneedtobewrittentodisk.Thatsaid,ifyoucareaboutthemarginalperformancegainsthat
thisoffers,thenyoumayneedtoconsiderwhetherthebrowseristhebestenvironmentforyourapp.
Listing6-7showshowIhaveaddedsupportforsessionpersistencetomyobservabledataitemin
utils.js.
Listing6-7.DefiningaSemi-persistentObservableDataItemUsingSessionStorage
ko.persistentObservable = function(keyName, initialValue, useSession) {
var storageObject = useSession ? sessionStorage : localStorage
var obItem = ko.observable(storageObject[keyName] || initialValue);
148
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
}
$(window).bind("storage", function(e) {
if (e.originalEvent.key == keyName) {
obItem(e.originalEvent.newValue);
}
});
obItem.subscribe(function(newValue) {
storageObject[keyName] = newValue;
});
return obItem;
SincethesessionStorageandlocalStorageobjectsexposethesamefeaturesandusethesame
event,Iamabletoeasilymodifymylocalstorageobservableitemtoaddsupportforsessionstorage.I
haveaddedanargumenttothefunctionthat,iftrue,switchestosessionstorage.Iuselocalstorageifthe
argumentisnotprovidedorisfalse.Listing6-8showshowIhaveappliedsessionstoragetotwoofthe
observabledataitemsintheexampleviewmodel.
Listing6-8.UsingSessionStorage
...
var viewModel = {
personalDetails: [
{name: "name", label: "Name", value: ko.persistentObservable("name")},
{name: "city", label: "City",
value: ko.persistentObservable("city", null, true)},
{name: "country", label: "Country",
value: ko.persistentObservable("country", null, true)}
]
};
...
ThevaluesoftheCityandCountryelementsarehandledusingsessionstoragewhiletheName
elementremainswithlocalstorage.Ifyouloadtheexampleintothebrowser,youwillfindthatreloading
thedocumentdoesn’tclearanyofthevaluesyouhaveentered.However,onlytheNamevalueremainsif
youcloseandreopenthedocument.
UsingLocalStoragewithOfflineWebApplications
Partofthebenefitthatcomesfromusinglocalstorageisthatitisavailableoffline.Thismeansthatwe
canuselocaldatatoaddresstheproblemsarisingfromAjaxGETrequestswhenthebrowserisoffline.
Listing6-9showsthecachedCheeseLuxwebappfromthepreviouschapter,updatedtotakeadvantage
oflocalstorage.
Listing6-9.UsingLocalStorageforOfflineWebAppsThatUseAjax
<!DOCTYPE html>
<html manifest="cheeselux.appcache">
<head>
<title>CheeseLux</title>
<link rel="stylesheet" type="text/css" href="styles.css"/>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
149
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
<script src="jquery-ui-1.8.16.custom.js" type="text/javascript"></script>
<script src='knockout-2.0.0.js' type='text/javascript'></script>
<script src='utils.js' type='text/javascript'></script>
<script src='signals.js' type='text/javascript'></script>
<script src='hasher.js' type='text/javascript'></script>
<script src='crossroads.js' type='text/javascript'></script>
<link rel="stylesheet" type="text/css" href="jquery-ui-1.8.16.custom.css"/>
<noscript>
<meta http-equiv="refresh" content="0; noscript.html"/>
</noscript>
<script>
var cheeseModel = {
cache: {
status: ko.observable(window.applicationCache.status),
online: ko.observable(window.navigator.onLine)
}
};
$.getJSON("products.json", function(data) {
cheeseModel.products = data;
localStorage["jsondata"] = JSON.stringify(data);
}).error(function() {
if (localStorage["jsondata"]) {
cheeseModel.products = JSON.parse(localStorage["jsondata"]);
}
}).complete(function() {
$(document).ready(function() {
if (cheeseModel.products) {
enhanceViewModel();
ko.applyBindings(cheeseModel);
hasher.initialized.add(crossroads.parse, crossroads);
hasher.changed.add(crossroads.parse, crossroads);
hasher.init();
crossroads.addRoute("category/:cat:", function(cat) {
cheeseModel.selectedCategory(cat ||
cheeseModel.products[0].category);
});
$('#buttonDiv input:submit').button();
$('div.navSelectors').buttonset();
$(window).bind("online offline", function() {
cheeseModel.cache.online(window.navigator.onLine);
});
$(window.applicationCache).bind("checking noupdate downloading " +
"progress cached updateready", function(e) {
cheeseModel.cache.status(window.applicationCache.status);
});
$('div.tagcontainer a').button().filter(':not([href])')
150
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
.click(function(e) {
e.preventDefault();
if ($(this).attr("data-action") == "update") {
window.applicationCache.update();
} else {
window.applicationCache.swapCache();
window.location.reload(false);
}
});
} else {
var dialogHTML = '<div>Try again later</div>';
$(dialogHTML).dialog({
modal: true,
title: "Ajax Error",
buttons: [{text: "OK",
click: function() {$(this).dialog("close")}}]
});
}
});
});
</script>
</head>
<body>
<div id="logobar">
<img src="cheeselux.png">
<div class="tagcontainer">
<span id="tagline">Gourmet European Cheese</span>
<div>
<span data-bind="visible: cheeseModel.cache.online()">
<a data-bind="visible: cheeseModel.cache.status() != 4"
data-action="update" class="cachelink">Check for Updates</a>
<a data-bind="visible: cheeseModel.cache.status() == 4"
data-action="swapCache" class="cachelink">Apply Update</a>
<a class="cachelink" href="/news.html">News</a>
</span>
<span data-bind="visible: !cheeseModel.cache.online()">
(Offline)
</span>
</div>
</div>
</div>
<div class="cheesegroup">
<div class="navSelectors" data-bind="foreach: products">
<a data-bind="formatAttr: {attr: 'href', prefix: '#category/',
value: category},
css: {selectedItem: (category == cheeseModel.selectedCategory())}">
<span data-bind="text: category">
</a>
</div>
</div>
151
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
<form action="/shipping" method="post">
<div data-bind="foreach: products">
<div class="cheesegroup"
data-bind="fadeVisible: category == cheeseModel.selectedCategory()">
<div class="grouptitle" data-bind="text: category"></div>
<!-- ko foreach: items -->
<div class="groupcontent">
<label data-bind="attr: {for: id}" class="cheesename">
<span data-bind="text: name">
</span> $(<span data-bind="text:price"></span>)</label>
<input data-bind="attr: {name: id}, value: quantity"/>
<span data-bind="visible: subtotal" class="subtotal">
($<span data-bind="text: subtotal"></span>)
</span>
</div>
<!-- /ko -->
<div class="groupcontent">
<label class="cheesename">Total:</label>
<span class="subtotal" id="total">
$<span data-bind="text: cheeseModel.total()"></span>
</span>
</div>
</div>
</div>
<div id="buttonDiv">
<input type="submit" value="Submit Order"/>
</div>
</form>
</body>
</html>
Inthislisting,IusetheJSON.stringifymethodtostoreacopyoftheviewmodeldatawhentheAjax
requestissuccessful:
$.getJSON("products.json", function(data) {
cheeseModel.products = data;
localStorage["jsondata"] = JSON.stringify(data);
})
Iaddedtheproducts.jsonURLtotheNETWORKsectionofthemanifestforthiswebapp,soIhavea
reasonableexpectationthatthedatawillbeavailableandthattheAjaxrequestwillsucceed.
If,however,therequestfails,whichwilldefinitelyhappenifthebrowserisoffline,thenItryto
locateandrestoretheserializeddatafromlocalstorage,likethis:
}).error(function() {
if (localStorage["jsondata"]) {
cheeseModel.products = JSON.parse(localStorage["jsondata"]);
}
})
Assumingtheinitialrequestworks,Iwillhaveagoodfallbackpositionifsubsequentrequestsfail.
TheeffectthatthistechniquecreatesissimilartothewaythatFirefoxhandlesAjaxrequestswhenthe
browserisofflinebecauseIendupusingthelastversionofthedataIwasabletoobtainfromtheserver.
152
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
NoticethatIhaverestructuredthecodesothattherestofthewebappsetupoccursinthecomplete
handlerfunction,whichistriggeredirrespectiveoftheoutcomeoftheAjaxrequest.Thesuccessor
failureofAjaxnolongerdetermineshowIprocessedit;nowitisallaboutwhetherornotIhavedata,
eitherfreshfromtheserverorrestoredfromlocalstorage.
UsingLocalStoragewithOfflineForms
ImentionedinChapter5thattheonlywayofdealingwithPOSTrequestsinacachedapplicationisto
preventtheuserfrominitiatingtherequestwhenthebrowserisoffline.Thisremainstrue,butyoucan
improvetheexperiencethatyoudelivertotheuserbyusinglocalstoragetocreatepersistentvalues.To
demonstratethisapproach,IfirstneedtoupdatetheenhanceViewModelfunctionintheutils.jsfileto
uselocalstoragetopersisttheformvalues,asshowninListing6-10.
Listing6-10.UpdatingtheenhanceViewModelFunctiontoUseLocalStorage
...
function enhanceViewModel() {
cheeseModel.selectedCategory
= ko.persistentObservable("selectedCategory", cheeseModel.products[0].category);
mapProducts(function(item) {
item.quantity = ko.persistentObservable(item.id + "_quantity", 0);
item.subtotal = ko.computed(function() {
return this.quantity() * this.price;
}, item);
}, cheeseModel.products, "items");
};
...
cheeseModel.total = ko.computed(function() {
var total = 0;
mapProducts(function(elem) {
total += elem.subtotal();
}, cheeseModel.products, "items");
return total;
});
Thisisaprettysimplechange,butthereareacoupleofpointstonote.Iwanttomaketheview
modelquantitypropertypersistentforeachcheeseproduct,soIusethevalueoftheitemidpropertyto
avoidkeycollisioninlocalstorage:
item.quantity = ko.persistentObservable(item.id + "_quantity", 0);
ThesecondpointtonoteisthatwhenIloadvaluesfromlocalstorage,Iwillbeputtingstrings,and
notnumbers,intheviewmodel.However,JavaScriptiscleverenoughtoconvertstringswhen
performingmultiplicationoperations,likethis:
return this.quantity() * this.price;
EverythingworksasIwouldlikeittowork.However,JavaScriptusesthesamesymboltodenote
stringconcatenationandnumericaddition,soifIhadbeentryingtosumvaluesintheviewmodel,I
wouldhavehadtotaketheextrastepofparsingthevalue,likethis:
153
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
return Number(this.quantity()) + someOtherValue;
UsingPersistenceintheOfflineApplication
NowthatIhavemodifiedtheviewmodel,IcanchangethemaindocumenttoimprovethewaythatI
handletheformelementwhenthebrowserisoffline.Listing6-11showsthechangestotheHTML
markup.
Listing6-11.AddingButtonsThatHandletheFormWhentheBrowserIsOffline
...
<form action="/shipping" method="post">
<div data-bind="foreach: products">
<div class="cheesegroup"
data-bind="fadeVisible: category == cheeseModel.selectedCategory()">
<div class="grouptitle" data-bind="text: category"></div>
<!-- ko foreach: items -->
<div class="groupcontent">
<label data-bind="attr: {for: id}" class="cheesename">
<span data-bind="text: name">
</span> $(<span data-bind="text:price"></span>)</label>
<input data-bind="attr: {name: id}, value: quantity"/>
<span data-bind="visible: subtotal" class="subtotal">
($<span data-bind="text: subtotal"></span>)
</span>
</div>
<!-- /ko -->
<div class="groupcontent">
<label class="cheesename">Total:</label>
<span class="subtotal" id="total">
$<span data-bind="text: cheeseModel.total()"></span>
</span>
</div>
</div>
</div>
<div id="buttonDiv">
<input type="submit" value="Submit Order"
data-bind="visible: cheeseModel.cache.online()"/>
<input type="button" value="Save for Later"
data-bind="visible: !cheeseModel.cache.online()"/>
</div>
</form>
...
IhaveaddedaSaveforLaterbuttontothedocument,whichisvisiblewhenthebrowserisoffline.I
havealsochangedthesubmitbuttonsothatitisvisibleonlywhenthebrowserisonline.Listing6-12
showsthecorrespondingchangestothescriptelement.
Listing6-12.ChangestothescriptElementtoSupportOfflineForms
<script>
var cheeseModel = {
154
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
};
cache: {
status: ko.observable(window.applicationCache.status),
online: ko.observable(window.navigator.onLine)
}
$.getJSON("products.json", function(data) {
cheeseModel.products = data;
localStorage["jsondata"] = JSON.stringify(data);
}).error(function() {
if (localStorage["jsondata"]) {
cheeseModel.products = JSON.parse(localStorage["jsondata"]);
}
}).complete(function() {
$(document).ready(function() {
if (cheeseModel.products) {
enhanceViewModel();
ko.applyBindings(cheeseModel);
hasher.initialized.add(crossroads.parse, crossroads);
hasher.changed.add(crossroads.parse, crossroads);
hasher.init();
crossroads.addRoute("category/:cat:", function(cat) {
cheeseModel.selectedCategory(cat ||
cheeseModel.products[0].category);
});
$('#buttonDiv input').button().click(function(e) {
if (e.target.type == "button") {
createDialog("Basket Saved for Later");
} else {
localStorage.clear();
}
});
$('div.navSelectors').buttonset();
$(window).bind("online offline", function() {
cheeseModel.cache.online(window.navigator.onLine);
});
$(window.applicationCache).bind("checking noupdate downloading " +
"progress cached updateready", function(e) {
cheeseModel.cache.status(window.applicationCache.status);
});
$('div.tagcontainer a').button().filter(':not([href])')
.click(function(e) {
e.preventDefault();
if ($(this).attr("data-action") == "update") {
window.applicationCache.update();
} else {
155
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
window.applicationCache.swapCache();
window.location.reload(false);
});
});
});
</script>
}
} else {
createDialog("Try again later");
}
Thisisasimplechange,andyou’llquicklyrealizethatIamdoingsomemildmisdirection.Whenthe
browserisonline,theusercansubmittheformasnormal,andanydatainlocalstorageiscleared.The
misdirectioncomeswhenthebrowserisofflineandtheuserclickstheSaveforLaterbutton.AllIdois
callthecreateDialogfunction,tellingtheuserthattheformdatahasbeensaved.However,Idon’t
actuallyneedtosavethedatabecauseIamusingpersistentobservabledataitemsintheviewmodel.
Theuserdoesn’tneedtoknowaboutthis;theyjustgetthebenefitofthepersistenceandaclearsignal
fromthewebapplicationthattheformdatahasnotbeensubmitted.Whenthebrowserisonlineagain,
theusercansubmitthedata.Usinglocalstorageallofthetimemeansthattheuserwon’tlosetheirdata
iftheycloseandlaterreloadtheapplicationbeforebeingabletosubmittheformtotheserver.For
completeness,Listing6-13showsthecreateDialogfunction,whichIdefinedintheutils.jsfile.Thisis
thesameapproachIusedtocreateanerrordialogintheoriginalexample,andImovedthecodeintoa
functionbecauseIneededtocreatethesamekindofdialogboxatmultiplepointsintheapplication.
Listing6-13.ThecreateDialogFunction
function createDialog(message) {
$('<div>' + message + '</div>').dialog({
modal: true,
title: "Message",
buttons: [{text: "OK",
click: function() {$(this).dialog("close")}}]
});
};
Ihavetakenaverysimpleanddirectapproachtodealingwithformdatawhenthebrowseris
offline,butyoucaneasilyseehowamoresophisticatedapproachcouldbecreated.Youmight,for
example,respondtotheonlineeventbypromptingtheusertosubmitthedataorevensubmitit
automaticallyusingAjax.Whateverapproachyoutake,youmustensurethattheuserunderstandsand
approvesofwhatyourwebappisdoing.
StoringComplexData
Storingname/valuepairsisperfectlysuitedtostoringformdata,butforanythingmoresophisticated,
suchasimpleapproachstartstobreakdown.Thereisanotherbrowserfeature,calledIndexedDB,which
youcanusetostoreandworkwithmorecomplexdata.
156
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
NoteIndexedDBisonlyoneoftwocompetingstandardsforstoringcomplexdatainthebrowser.Theotheris
WebSQL.AsIwritethis,theW3CissupportingIndexedDB,butitisentirelypossiblethatWebSQLwillmakea
comebackor,atleast,becomeadefactostandard.IhavenotincludedWebSQLinthischapterbecausesupport
foritislimitedatpresent,butthisisanareaoffunctionalitythatisfarfromsettled,andyoushouldreviewthe
supportforbothstandardsbeforeadoptingoneofthemforyourprojects.
ItisstillearlydaysforIndexedDB,andasIwritethis,thefunctionalityisavailableonlythrough
vendor-specifiedprefixes,signifyingthatthebrowserimplementationsarestillexperimentalandmay
deviatefromtheW3Cspecification.Currently,thebrowserthatadheresmostcloselytotheW3C
specificationisMozillaFirefox,sothisisthebrowserIhaveusedtodemonstrateIndexedDB.
CautionTheexamplesinthischaptermaynotworkwithbrowsersotherthanFirefox.Infact,theymaynot
workevenwithversionsofFirefoxotherthantheoneIusedinthischapter(version10).Thatsaid,youshouldstill
beabletogetasolidunderstandingofhowIndexedDBworks,evenifthespecificationorimplementationschange.
TheIndexedDBfeatureisorganizedarounddatabasesthat,likelocalandsessionstorage,are
isolatedonaper-originbasissothattheycanbesharedbetweenapplicationsfromthesameorigin.
IndexedDBdoesn’tfollowtheSQL-basedtablestructurethatiscommoninrelationaldatabases.An
IndexedDBdatabaseismadeupofobjectstores,whichcancontainJavaScriptobjects.Youcanadd
JavaScriptobjectstoobjectstores,andyoucanquerythosestoresindifferentways,someofwhichI
demonstrateshortly.
Theresultofthisapproachisastoragemechanismthatismoreinkeepingwiththestyleofthe
JavaScriptlanguagebutthatendsupbeingslightlyawkwardtouse.AlmostalloperationsinIndexedDB
areperformedasasynchronousrequeststowhichfunctionscanbeattachedsothattheyareexecuted
whentheoperationcompletes.TodemonstratehowIndexedDBworks,IamgoingtocreateaCheese
Finderapplication.IwillputthecheeseproductdataintoanIndexedDBdatabaseandprovidetheuser
withsomedifferentwaysofsearchingthedataforcheesestheymightlike.Figure6-2showsthefinished
webapptohelpprovidesomecontextforthecodethatfollows.
157
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
Figure6-2.UsingIndexedDBtoqueryproductdata
Thefigureshowstheoptiontosearchthedescriptionofeachproductinuse.Ihavesearchedforthe
termcow,andthoseproductswhosedescriptionscontainthistermarelistedatthebottomofthepage.
(Thereareseveralmatchesbecausemanyofthedescriptionsexplainthatthecheeseismadefromcows’
milk.)
CreatingtheIndexedDBDatabaseandObjectStore
Thecodeforthisexampleissplitbetweentheutils.jsfileandthemainexample.htmldocument.I’llbe
jumpingbetweenthesefilestodemonstratethecorefeaturesthatIndexedDBoffers.Tobegin,Ihave
definedaDBOobjectandthesetupDatabasefunctioninutils.js,asshowninListing6-14.
158
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
Listing6-14.SettingUptheIndexedDBDatabase
var DBO = {
dbVersion: 31
}
function setupDatabase(data, callback) {
var indexDB = window.indexedDB || window.mozIndexedDB;
var req = indexDB.open("CheeseDB", DBO.dbVersion);
req.onupgradeneeded = function(e) {
var db = req.result;
var existingStores = db.objectStoreNames;
for (var i = 0; i < existingStores.length; i++) {
db.deleteObjectStore(existingStores[i]);
}
var objectStore = db.createObjectStore("products", {keyPath: "id"});
objectStore.createIndex("category", "category", {unique: false});
};
};
$.each(data, function(index, item) {
var currentCategory = item.category;
$.each(item.items, function(index, item) {
item.category = currentCategory;
objectStore.add(item);
});
});
req.onsuccess = function(e) {
DBO.db = this.result;
callback();
};
IhavedefinedanobjectcalledDBOthatperformstwoimportanttasks.First,itdefinestheversionof
thedatabasethatIamexpectingtoworkwith.EachtimeImakeachangetothedatabaseschema,I
incrementthevalueofthedbVersionproperty,andasyoucansee,ittookme31changesuntilIgotthe
resultIwantedforthisexample.Thiswaslargelybecauseofthedifferencesbetweenthecurrentdraftof
thespecificationandtheimplementationinFirefox.
TipTheversionnumberisanimportantmechanisminensuringIamworkingwiththerightversionofthe
schemaformyapp.I’llshowyouhowtochecktheschemaversionand,ifneeded,upgradetheschema,shortly.
159
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
InthesetupDatabasefunction,Ibeginbylocatingtheobjectthatactsasthegatewaytothe
IndexedDBdatabases,likethis:
var indexDB = window.indexedDB || window.mozIndexedDB;
TheIndexedDBfeatureisavailableinFirefoxonlythroughthewindow.mozIndexedDBobjectatthe
moment,butthatwillchangetowindow.indexedDBoncetheimplementationconvergesonthefinal
specification.Togiveyouthegreatestchanceofmakingtheexamplesinthispartofthechapterwork,I
trytousethe“official”IndexedDBobjectfirstandfallbacktothevendor-prefixedalternativeifitisn’t
available.Thenextstepistoopenthedatabase:
var req = indexDB.open("CheeseDB", DBO.dbVersion);
Thetwoargumentsarethenameofthedatabaseandtheexpectedschemaversion.IndexedDBwill
openthespecifieddatabaseifitalreadyexistsandcreateitifitdoesn’t.Theresultfromtheopenmethod
isanobjectthatrepresentstherequesttoopenthedatabase.TogetanythingdoneinIndexedDB,you
mustsupplyhandlerfunctionsforoneormoreofthepossibleoutcomesfromarequest.
RespondingtotheUpgrade-NeededOutcome
IcareabouttwopossibleoutcomeswhenIopenthedatabase.First,Iwanttobenotifiedifthedatabase
alreadyexistsandtheschemaversiondoesn’tmatchtheversionIamexpecting.Whenthishappens,I
wanttodeletetheobjectstoresinthedatabaseandstartover.Ireceivenotificationofaschema
mismatchbyregisteringafunctionthroughtheonupgradeneededproperty:
req.onupgradeneeded = function(e) {
var db = req.result;
var existingStores = db.objectStoreNames;
for (var i = 0; i < existingStores.length; i++) {
db.deleteObjectStore(existingStores[i]);
}
var objectStore = db.createObjectStore("products", {keyPath: "id"});
objectStore.createIndex("category", "category", {unique: false});
};
$.each(data, function(index, item) {
var currentCategory = item.category;
$.each(item.items, function(index, item) {
item.category = currentCategory;
objectStore.add(item);
});
});
Thedatabaseobjectisavailablethroughtheresultpropertyoftherequestreturnedbytheopen
method.IgetalistoftheexistingobjectstoresthroughtheobjectStoreNamespropertyanddeleteeach
inturnusingthedeleteObjectStoremethod.Indeletingtheobjectstores,Ialsodeletethedatathey
contain.Thisisfineforsuchasimplewebappwhereallofthedataiscomingfromtheserverandis
easilyreplaced,butyoumayneedtotakeamoresophisticatedapproachifyourdatabasescontaindata
thathasbeengeneratedasaresultofuseractions.
160
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
CautionThefunctionassignedtotheonupgradeneededpropertyistheonlyopportunityyouhavetomodifythe
schemaofthedatabase.Ifyoutrytoaddordeleteanobjectstoreelsewhere,thebrowserwillgenerateanerror.
Oncetheexistingobjectstoresareoutoftheway,Icancreatesomenewonesusingthe
createObjectstoremethod.Theargumentstothismethodarethenameofthenewstoreandan
optionalobjectcontainingconfigurationsettingstobeappliedtothenewstore.IhaveusedthekeyPath
configurationoption,whichletsmesetadefaultkeyforobjectsthatareaddedtothestore.Ihave
specifiedtheidpropertyasthekey.IhavealsocreatedanindexusingthecreateIndexmethodonthe
newlycreatedobjectstore.Anindexallowsmetoperformsearchesintheobjectstoreusingaproperty
otherthanthekey,inthiscase,thecategoryproperty.I’llshowyouhowtouseanindexshortly.
Finally,Iaddobjectstothedatastore.WhenIusethisfunctioninthemaindocument,I’llbeusing
thedataIgetfromanAjaxrequestfortheproducts.jsonfile.ThisisinthesameformatasthedataIhave
beenusingthroughoutthisbook.IusethejQueryeachfunctiontoenumerateeachcategoryandthe
itemsitcontains.IhaveaddedacategorypropertytoeachitemsothatIcanfindalloftheproductsthat
belongtothesamecategorymoreeasily.
TipTheobjectsyouaddtoanobjectstoreareclonedusingtheHTML5structuredclonetechnique.Thisisa
morecomprehensiveserializationtechniquethanJSON,andthebrowserwillgenerallymanagetodealwith
complexobjects,justaslongasnoneofthepropertiesisafunctionorDOMAPIobject.
RespondingtotheSuccessOutcome
ThesecondoutcomeIcareaboutissuccess,whichIhandlebyassigningafunctiontotheonsuccess
propertyoftherequesttoopenthedatabase,asfollows:
req.onsuccess = function(e) {
DBO.db = this.result;
callback();
};
ThefirststatementinthisfunctionassignstheopeneddatabasetothedbpropertyoftheDBOobject.
ThisisjustaconvenientwaytokeepahandleonthedatabasesothatIcanuseitinotherfunctions,
somethingthatI’lldemonstrateshortly.
Thesecondstatementinvokesthecallbackfunctionthatwaspassedasthesecondargumenttothe
setupDatabasefunction.Itisn’tsafetoassumethatthedatabaseisopenuntiltheonsuccessfunctionis
executed,whichmeansIneedtohavesomemechanismforsignalingthefunctioncallerthatthe
databasehasbeensuccessfullyopenedanddata-relatedoperationscanbestarted.
161
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
TipIndexedDBrequestshaveacounterpartoutcomepropertycalledonerror.Iwon’tbedoinganyerror
handlingintheseexamplesbecause,asIwritethis,tryingtodealwithIndexedDBerrorscausesmoreproblems
thanitsolves.Ideally,thiswillhaveimprovedbythetimeyoureadthischapter,andyouwillbeabletowritemore
robustcode.
IncorporatingtheDatabaseintotheWebApplication
Listing6-15showsthemarkupandinlineJavaScriptfortheexampleapplication.Withtheexceptionof
thedatabase-specificfunctions,everythinginthisexamplereliesontopicscoveredinearlierchapters.
Listing6-15.TheDatabase-ConsumingWebApplication
<!DOCTYPE html>
<html>
<head>
<title>CheeseLux Cheese Finder</title>
<link rel="stylesheet" type="text/css" href="styles.css"/>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script src="jquery-ui-1.8.16.custom.js" type="text/javascript"></script>
<script src='knockout-2.0.0.js' type='text/javascript'></script>
<script src='utils.js' type='text/javascript'></script>
<script src='signals.js' type='text/javascript'></script>
<script src='hasher.js' type='text/javascript'></script>
<script src='crossroads.js' type='text/javascript'></script>
<link rel="stylesheet" type="text/css" href="jquery-ui-1.8.16.custom.css"/>
<noscript>
<meta http-equiv="refresh" content="0; noscript.html"/>
</noscript>
<script>
var viewModel = {
searchModes: ["ID", "Description", "Category"],
selectedMode: ko.observable("ID"),
selectedItems: ko.observableArray()
};
function handleSearchResults(resultData) {
if (resultData) {
viewModel.selectedItems.removeAll();
if ($.isArray(resultData)) {
for (var i = 0; i < resultData.length; i++) {
viewModel.selectedItems.push(resultData[i]);
}
} else {
viewModel.selectedItems.push(resultData);
}
}
162
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
}
$.getJSON("products.json", function(data) {
setupDatabase(data, function() {
$(document).ready(function() {
hasher.initialized.add(crossroads.parse, crossroads);
hasher.changed.add(crossroads.parse, crossroads);
hasher.init();
crossroads.addRoute("mode/:mode:", function(mode) {
viewModel.selectedMode(mode || viewModel.searchModes[0]);
viewModel.selectedItems.removeAll();
$('#textsearch').val("");
});
crossroads.parse(location.hash.slice(1));
});
});
ko.applyBindings(viewModel);
$('div.navSelectors').buttonset();
$('div.groupcontent a').button().click(function() {
var sText = $('#textsearch').val();
switch (viewModel.selectedMode()) {
case "ID":
getProductByID(sText, handleSearchResults)
break;
case "Description":
getProductsByDescription(sText, handleSearchResults);
break;
case "Category":
getProductsByCategory(sText, handleSearchResults);
break;
};
});
});
</script>
</head>
<body>
<div id="logobar">
<img src="cheeselux.png">
<div class="tagcontainer">
<span id="tagline">Cheese Finder</span>
</div>
</div>
<div class="cheesegroup">
<div class="navSelectors" data-bind="foreach: searchModes">
<a data-bind="formatAttr: {attr: 'href', prefix: '#mode/', value: $data},
css: {selectedItem: $data == $root.selectedMode()}">
<span data-bind="text: $data">
</a>
163
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
</div>
</div>
<div class="cheesegroup">
<div class="grouptitle">Search Criteria</div>
<div class="groupcontent centered">
<label class="cheesename">Search Text:</label>
<input id="textsearch" class="stwin"/>
<a id="textsearch" class="smallbutton">Search</a>
</div>
</div>
<div class="cheesegroup">
<div class="grouptitle">Search Results</div>
<div class="groupcontent centered">
<table id="resultTable" data-bind="visible: selectedItems().length > 0">
<thead>
<tr><th>Name</th><th>Price</th><th>Description</th></tr>
<tr><td colspan=3 class="sumline"></td></tr>
</thead>
<tbody>
<!-- ko foreach: viewModel.selectedItems() -->
<tr>
<td data-bind="text: name"></td>
<td data-bind="text: price"></td>
<td data-bind="text: description"></td>
</tr>
<tr><td colspan=3 class="sumline"></td></tr>
<!-- /ko -->
</tbody>
</table>
<div data-bind="visible: selectedItems().length == 0">
No matches
</div>
</div>
</div>
</body>
</html>
Asyoumightexpectbynow,Ihaveusedaviewmodeltobindthestateoftheapplicationtothe
HTMLmarkup.Mostofthedocumentistakenupdefiningandcontrollingtheviewgiventotheuserand
supportinguserinteractions.
Whentheuserclicksthesearchbutton,oneofthreefunctionsintheutils.jsfileiscalled,
dependingontheselectedsearchmode.IftheuserhaselectedtosearchbyproductID,thenthe
getProductByIDfunctioniscalled.ThegetProductsByDescriptionfunctionisusedwhentheuserwants
tosearchtheproductdescriptions,andthegetProductsByCategoryfunctionisusedtofindallthe
productsinaspecificcategory.Eachofthesefunctionstakestwoarguments:thetexttosearchforanda
callbackfunctiontowhichtheresultsshouldbedispatched(evensearchinganobjectstoreisan
asynchronousoperationwithIndexedDB).Thecallbackfunctionisthesameforallthreesearchmodes:
handleSearchResults.Theresultfromthesearchfunctionswillbeasingleproductobjectoranarrayof
objects.ThejobofthehandleSearchResultsfunctionistoclearthecontentsoftheselectedItems
164
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
observablearrayintheviewmodelandreplacethemwiththenewresults;thiscausestheelementstobe
updatedandtheresultstobedisplayedtotheuser.
NoticethatIplacemostofthecodestatementsinmyinlinescriptelementinsidethecallbackfor
thesetupDatabasefunction.Thisisthefunctionthatiscalledwhenthedatabasehassuccessfullybeen
opened.
LocatinganObjectbyKey
ThefirstofthesearchfunctionsisgetProductByID,whichlocatesanobjectbasedonthevalueoftheid
property.YouwillrecallthatIspecifiedthispropertyasthekeyfortheobjectstorewhenIcreatedthe
database:
var objectStore = db.createObjectStore("products", {keyPath: "id"});
Gettinganobjectusingitskeyisprettysimple.Listing6-16showsthegetProductByIDfunction,
whichIdefinedintheutils.jsfile.
Listing6-16.LocatinganObjectUsingItsKey
function getProductByID(id, callback) {
var transaction = DBO.db.transaction(["products"]);
var objectStore = transaction.objectStore("products");
var req = objectStore.get(id);
req.onsuccess = function(e) {
callback(this.result);
};
}
Thisfunctionshowsthebasicpatternforqueryinganobjectstoreinadatabase.First,youmust
createatransaction,usingthetransactionmethod,declaringtheobjectsstoresthatyouwanttowork
with.Onlythencanyouopenanobjectstore,usingtheobjectStoremethodonthetransactionyoujust
created.
TipYoudon’tneedtoexplicitlycloseyourobjectstoreoryourtransactions;thebrowserclosesthemforyou
whentheyareoutofscope.Thereisnobenefitintryingtoexplicitlyforcethestoreortransactionstoclose.
Iobtaintheobjectwiththespecifiedkeyusingthegetmethod,whichmatchesatmostoneobject
(iftherearemultipleobjectswiththesamekey,thenthefirstmatchingobjectismatched).Themethod
returnsarequest,andImustsupplyafunctionfortheonsuccesspropertytobenotifiedwhenthesearch
hascompleted.Thematchedobjectisavailableintheresultpropertyoftherequest,whichIpassback
tothemainpartofthewebappbyinvokingthecallbackfunctionpassedtothegetProductByIDfunction
(which,asyouwillrecall,isthehandleSearchResultsfunction).
The(eventual)resultfromthegetmethodisaJavaScriptobjector,ifthereisnomatch,null.Idon’t
havetoworryaboutre-creatinganobjectfromtheserializeddatastoredbythedatabaseoruseanykind
ofobject-relationalmappinglayer.TheIndexedDBdatabaseworksonJavaScriptobjectsthroughout,
whichisanicefeature.
165
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
Itisalittlefrustratingtohavetousecallbackseverytimeyouwanttoperformasimpleoperation,
butitquicklybecomessecondnature.Theresultisastoragemechanismthatfitsnicelyintothe
JavaScriptworldandthatdoesn’ttieupthemainthreadofexecutionwhenlongoperationsarebeing
performedbutthatrequirescarefulthoughtandapplicationdesigntobeproperlyused.
LocatingObjectsUsingaCursor
Ihavetotakeadifferentapproachwhentheuserwantstosearchforproductsbytheirdescription.
Descriptionsarenotakeyinmyobjectstore,andIwanttobeabletolookforpartialmatches(otherwise
theuserwouldhavetoexactlytypeinallofthedescriptiontomakeamatch).Listing6-17showsthe
getProductsByDescriptionfunction,whichisdefinedinutils.js.
Listing6-17.LocatingObjectsUsingaCursor
function getProductsByDescription(text, callback) {
var searchTerm = text.toLowerCase();
var results = [];
var transaction = DBO.db.transaction(["products"]);
var objectStore = transaction.objectStore("products");
objectStore.openCursor().onsuccess = function(e) {
var cursor = this.result;
if (cursor) {
if (cursor.value.description.toLowerCase().indexOf(searchTerm) > -1) {
results.push(cursor.value);
}
cursor.continue();
} else {
callback(results);
}
};
};
Mytechniquehereistouseacursortoenumeratealloftheobjectsintheobjectstoreandlookfor
thosewhoseproductspropertycontainsthesearchtermprovidedbytheuser.Acursorsimplykeeps
trackofmyprogressasIenumeratethroughasequenceofdatabaseobjects.
IndexedDBdoesn’thaveatextsearchfacility,soIhavetohandlethismyself.CallingtheopenCursor
methodonanobjectstorecreatesarequestwhoseonsuccesscallbackisexecutedwhenthecursoris
opened.Thecursoritselfisavailablethroughtheresultpropertyofthethiscontextobject.(Itshould
alsobeavailablethroughtheresultpropertyoftheeventpassedtothefunction,butthecurrent
implementationdoesn’talwayssetthisreliably.)
Ifthecursorisn’tnull,thenthereisanobjectavailableinthevalueproperty.Ichecktoseewhether
thedescriptionpropertyoftheobjectcontainsthetermIamlookingfor,andifitdoes,Ipushtheobject
intoalocalarray.Tomovethecursortothenextobject,Icallthecontinuemethod,whichexecutesthe
onsuccessfunctionagain.
ThecursorisnullwhenIhavereadalloftheobjectsintheobjectstore.Atthispoint,mylocalarray
containsalloftheobjectsthatmatchmysearch,andIpassthembacktothemainpartoftheweb
applicationusingthecallbacksuppliedasthesecondargumenttothegetProductsByDescription
function.
166
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
LocatingObjectsUsinganIndex
Enumeratingalloftheobjectsinanobjectstoreisn’tanefficientwayoffindingobjects,whichiswhyI
createdanindexforthecategorypropertywhenIsetuptheobjectstore:
objectStore.createIndex("category", "category", {unique: false});
TheargumentstothecreateIndexmethodarethenameoftheindex,thepropertyintheobjects
thatwillbeindexed,andaconfigurationobject,whichIhaveusedtotellIndexedDBthatthevaluesfor
thecategorypropertyarenotunique.
ThegetProductsByCategoryfunction,whichisshowninListing6-18,usestheindextonarrowthe
objectsthatareenumeratedbythecursor.
Listing6-18.UsinganIndexedDBIndex
function getProductsByCategory(searchCat, callback) {
var results = [];
var transaction = DBO.db.transaction(["products"]);
var objectStore = transaction.objectStore("products");
var keyRange = IDBKeyRange.only(searchCat);
var index = objectStore.index("category");
index.openCursor(keyRange).onsuccess = function(e) {
var cursor = this.result;
if (cursor) {
results.push(cursor.value);
cursor.continue();
} else {
callback(results);
}
};
};
TheIDBKeyRangeobjecthasanumberofmethodsforconstrainingthekeyvaluesthatwillmatch
objectsintheobjectstore.IhaveusedtheonlymethodtospecifythatIwantexactmatchesonly.
IopentheindexbycallingtheindexmethodontheobjectstoreandpassintheIDBKeyRangeobject
asanargumentwhenIopenthecursor.Thishastheeffectofnarrowingthesetofobjectsthatare
availablethroughthecursor,meaningthattheresultsIpassviathecallbackcontainonlythecheese
productsinthespecifiedcategory.Thereisnopartialmatchinginthisexample;theusermustenterthe
entirecategoryname,suchasFrenchCheese.
Summary
Inthischapter,Ishowedyouhowtouselocalstoragetopersistentlystorename/valuepairsinthe
browserandhowthisfeaturecanbeusedinanofflinewebapptodealwithHTMLforms.Ialsoshowed
youtheIndexedDBfeatures,whichisfarlessmaturebutshowspromiseasafoundationforstoringand
queryingmorecomplexdatausingnaturalJavaScriptobjectsandlanguageidioms.
IndexedDBisn’tyetreadyforproductionuse,butIfindthatlocalstorageisveryrobustandhelpful
inawiderangeofsituations.Ifinditespeciallyusefulinmakingformsmoreusefulandlessannoying,
muchasIdemonstratedinthischapter.Thelocalstoragefeatureisveryeasytouse,especiallywhenitis
embeddedwithinyourapplicationviewmodel.
167
www.it-ebooks.info
CHAPTER6STORINGDATAINTHEBROWSER
Inthenextchapter,Ishowyouhowtocreateresponsivewebappsthatadaptandrespondtothe
capabilitiesofthedevicesonwhichtheyrun.
168
www.it-ebooks.info
CHAPTER 7
Creating Responsive Web Apps
Therearetwoapproachestotargetingmultipleplatformswithawebapp.Thefirstistocreateadifferent
versionoftheappforeachkindofdeviceyouwanttotarget:desktop,smartphone,tablet,andsoon.I’ll
giveyousomeexamplesofhowtodothisinChapter8.
Theotherapproach,andthetopicofthischapter,istocreatearesponsivewebapp,whichsimply
meansthatthewebappadaptstothecapabilitiesofthedeviceitisrunningon.Ilikethisapproach
becauseitdoesn’tdrawaharddistinctionbetweenmobileand“normal”devices.
Thisisimportantbecausethecapabilitiesofsmartphones,tablets,anddesktopsblurtogether.
ManymobilebrowsersalreadyhavegoodHTML5support,anddesktopmachineswithtouchscreensare
becomingmorecommon.Inthischapter,I’llshowyoutechniquesthatyoucanusetocreateweb
applicationsthatareflexibleandfluid.
SettingtheViewport
Ineedtoaddressoneissuethatisspecifictothebrowsersrunningonsmartphonesandtablets(which
I’llstartreferringtoasmobilebrowsers).Mobilebrowserstypicallystartfromtheassumptionthata
websitewillhavebeendesignedforalarge-screeneddesktopdeviceandthat,asaconsequence,theuser
willneedsomehelptobeabletoviewit.Thisisdonethroughtheviewport,whichscalesdowntheweb
pagesothattheusergetsasenseoftheoverallpagestructure.Theuserthenzoomsintoaparticular
regionofthepageinordertoreadoruseit.YoucanseetheeffectinFigure7-1.
Figure7-1.Theeffectofthedefaultviewportinamobilebrowser
169
www.it-ebooks.info
CHAPTER7CREATINGRESPONSIVEWEBAPPS
NoteThescreenshotsinFigure7-1areoftheOperaMobileemulator,whichyoucangetfrom
www.opera.com/developer/tools/mobile.Althoughithassomequirks,thisemulatorisreasonablyfaithfulto
therealOperaMobile,whichiswidelyusedinmobiledevices.Ilikeitbecauseitallowsmetocreateemulators
withscreensizesrangingfromsmallsmartphonestolargetabletsandtoselectwhethertoucheventsare
supported.Asabonus,youcandebugandinspectyourwebappusingthestandardOperadevelopmenttools.An
emulatorisnosubstitutefortestingonarangeofrealhardwaredevicesbutcanbeveryconvenientduringthe
earlystagesofdevelopment.
Thisisasensiblefeature,butyouneedtodisableitforwebapps;otherwise,contentandcontrols
aredisplayedatasizethatistoosmalltouse.Listing7-1showshowtodisablethisfeatureusingthe
HTMLmetatag,whichIhaveappliedtoasimplifiedversionoftheCheeseLuxwebapp,whichwillbethe
foundationexampleforthischapter.
Listing7-1.UsingthemetaTagtoControltheViewportintheCheeseLuxWebApp
<!DOCTYPE html>
<html>
<head>
<title>CheeseLux</title>
<link rel="stylesheet" type="text/css" href="styles.css"/>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script src="jquery-ui-1.8.16.custom.js" type="text/javascript"></script>
<script src='knockout-2.0.0.js' type='text/javascript'></script>
<script src='utils.js' type='text/javascript'></script>
<script src='signals.js' type='text/javascript'></script>
<script src='crossroads.js' type='text/javascript'></script>
<script src='hasher.js' type='text/javascript'></script>
<script src='modernizr-2.0.6.js' type='text/javascript'></script>
<link rel="stylesheet" type="text/css" href="jquery-ui-1.8.16.custom.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script>
var cheeseModel = {};
$.getJSON("products.json", function(data) {
cheeseModel.products = data;
}).success(function() {
$(document).ready(function() {
$('#buttonDiv input:submit').button().css("font-family", "Yanone");
$('div.cheesegroup').not("#basket").css("width", "50%");
$('div.navSelectors').buttonset();
enhanceViewModel();
ko.applyBindings(cheeseModel);
hasher.initialized.add(crossroads.parse, crossroads);
hasher.changed.add(crossroads.parse, crossroads);
170
www.it-ebooks.info
CHAPTER7CREATINGRESPONSIVEWEBAPPS
hasher.init();
crossroads.addRoute("category/:newCat:", function(newCat) {
cheeseModel.selectedCategory(newCat ?
newCat : cheeseModel.products[0].category);
});
crossroads.parse(location.hash.slice(1));
});
});
</script>
</head>
<body>
<div id="logobar">
<img src="cheeselux.png">
<span id="tagline">Gourmet European Cheese</span>
</div>
<div class="cheesegroup">
<div class="navSelectors" data-bind="foreach: products">
<a data-bind="formatAttr: {attr: 'href', prefix: '#category/',
value: category},
css: {selectedItem: (category == cheeseModel.selectedCategory())}">
<span data-bind="text: category">
</a>
</div>
</div>
<div id="basket" class="cheesegroup basket">
<div class="grouptitle">Basket</div>
<div class="groupcontent">
<div class="description" data-bind="ifnot: total">
No products selected
</div>
<table id="basketTable" data-bind="visible: total">
<thead><tr><th>Cheese</th><th>Subtotal</th><th></th></tr></thead>
<tbody data-bind="foreach: products">
<!-- ko foreach: items -->
<tr data-bind="visible: quantity, attr: {'data-prodId': id}">
<td data-bind="text: name"></td>
<td>$<span data-bind="text: subtotal"></span></td>
</tr>
<!-- /ko -->
</tbody>
<tfoot>
<tr><td class="sumline" colspan=2></td></tr>
<tr>
<th>Total:</th><td>$<span data-bind="text: total"></span></td>
</tr>
</tfoot>
</table>
171
www.it-ebooks.info
CHAPTER7CREATINGRESPONSIVEWEBAPPS
</div>
<div class="cornerplaceholder"></div>
<div id="buttonDiv">
<input type="submit" value="Submit Order"/>
</div>
</div>
<form action="/shipping" method="post">
<!-- ko foreach: products -->
<div class="cheesegroup"
data-bind="fadeVisible: category == cheeseModel.selectedCategory()">
<div class="grouptitle" data-bind="text: category"></div>
<div data-bind="foreach: items">
<div class="groupcontent">
<label data-bind="attr: {for: id}" class="cheesename">
<span data-bind="text: name">
</span> $(<span data-bind="text:price"></span>)</label>
<input data-bind="attr: {name: id}, value: quantity"/>
<span data-bind="visible: subtotal" class="subtotal">
($<span data-bind="text: subtotal"></span>)
</span>
</div>
</div>
</div>
<!-- /ko -->
</form>
</body>
</html>
Addingthehighlightedmetaelementtothedocumentdisablesthescalingfeature.Youcanseethe
effectinFigure7-2.ThisparticularmetatagtellsthebrowsertodisplaytheHTMLdocumentusingthe
actualwidthofthedisplayandwithoutanymagnification.Ofcourse,thewebappisstillamess,butitis
amessthatisbeingdisplayedatthecorrectsize,whichisthefirststeptowardaresponsiveapp.Inthe
restofthischapter,I’llshowyouhowtorespondtodifferentdevicecharacteristicsandcapabilities.
Figure7-2.Theeffectofdisablingtheviewportforawebapp
172
www.it-ebooks.info
CHAPTER7CREATINGRESPONSIVEWEBAPPS
RespondingtoScreenSize
MediaqueriesareausefulwayoftailoringCSSstylestothecapabilitiesofthedevice.Perhapsthemost
importantcharacteristicofadevicefromtheperspectiveofaresponsivewebappisscreensize,which
CSSmediaqueriesaddressverywell.AsFigure7-2shows,theCheeseLuxlogotakesupalotofspaceon
asmallscreen,andIcanuseaCSSmediaquerytoensurethatitisshownonlyonlargerdisplays.Listing
7-2showsasimplemediaquerythatIaddedtothestyles.cssfile.
Listing7-2.ASimpleMediaQuery
@media screen AND (max-width:500px) {
*.largeScreenOnly {
display: none;
}
}
TipOperaMobileaggressivelycachesCSSandJavaScriptfiles.Whenexperimentingwithmediaqueries,the
besttechniqueistodefinetheCSSandscriptcodeinthemainHTMLdocumentandmoveittoexternalfileswhen
youarehappywiththeresult.Otherwise,youwillneedtoclearthecache(orrestarttheemulator)toensureyour
changesareapplied.
The@mediatagtellsthebrowserthatthisisamediaquery.IhavespecifiedthatthelargeScreenOnly
stylecontainedinthisqueryshouldbeappliedonlyifthedeviceisascreen(asopposedtoaprojectoror
printedmaterial)andthewidthisnogreaterthan500pixels.
TipInthischapter,Iamgoingtodividetheworldintotwocategoriesofdisplays.Smalldisplayswillbethose
whosewidthisnogreaterthan500pixels,andlargedisplayswillbeeverythingelse.Thisissimpleandarbitrary,
andyoumayneedtodevisemorecategoriestogettheeffectyourequireforyourwebapp.Iamgoingtoignore
theheightofthedisplayentirely.Mysimplecategorieswillkeeptheexamplesinthischaptermanageable,albeit
atthecostofgranularity.
Iftheseconditionsaremet,thenastyleisdefinedthatsetstheCSSdisplaypropertyforanyelement
assignedtothelargeScreenOnlyclasstonone,whichhidestheelementfromview.Withtheadditionto
thestylesheet,IcanensurethattheCheeseLuxlogoisshownonlyonlargedisplaysbyapplyingthe
largeScreenOnlyclasstomymarkup,asshowninListing7-3.
173
www.it-ebooks.info
CHAPTER7CREATINGRESPONSIVEWEBAPPS
Listing7-3.UsingCSSMediaQueriestoRespondtoScreenSizes
...
<div id="logobar" class="largeScreenOnly">
<img src="cheeselux.png">
<span id="tagline">Gourmet European Cheese</span>
</div>
...
CSSmediaqueriesarelive,whichmeansthecategoryofscreensizecanchangeifthebrowser
windowisresized.Thisisn’tmuchuseonmobiledevices,butitmeansthataresponsivewebappwill
adapttothedisplaysizeevenonadesktopplatform.YoucanseehowthelayoutsalterinFigure7-3.
Figure7-3.Usingmediaqueriestomanagethevisibilityofelements
UsingMediaQuerieswithJavaScript
Toproperlyintegratemediaqueriesintoawebapp,weneedtousetheViewmoduleoftheW3CCSS
ObjectModelspecification,whichbringsJavaScriptmediaqueriessupportintothebrowser.Media
queriesareevaluatedinJavaScriptusingthewindow.matchMediamethod,asshowninListing7-4.Ihave
definedthedetectDeviceFeaturesfunctionintheutils.jsfile;atthemoment,itdetectsonlythescreen
size,butI’lldetectsomeadditionalfeatureslater.Thereisalotgoingoninthelisting,soI’llbreakit
downandexplainthevariouspartsinthesectionsthatfollow.
Listing7-4.UsingaMediaQueryinJavaScript
function detectDeviceFeatures(callback) {
var deviceConfig = {};
Modernizr.load({
test: window.matchMedia,
nope: 'matchMedia.js',
complete: function() {
174
www.it-ebooks.info
CHAPTER7CREATINGRESPONSIVEWEBAPPS
var screenQuery = window.matchMedia('screen AND (max-width:500px)');
deviceConfig.smallScreen = ko.observable(screenQuery.matches);
if (screenQuery.addListener) {
screenQuery.addListener(function(mq) {
deviceConfig.smallScreen(mq.matches);
});
}
deviceConfig.largeScreen = ko.computed(function() {
return !deviceConfig.smallScreen();
});
setInterval(function() {
deviceConfig.smallScreen(window.innerWidth <= 500);
}, 500);
callback(deviceConfig);
};
});
}
LoadingthePolyfill
IneedtouseapolyfilltomakesureIcanusethematchMediamethod.Supportforthisfeatureisgoodin
desktopbrowsersbutspottyinthemobileworld.ThepolyfillIuseiscalledmatchMedia.jsandis
availablefromhttp://github.com/paulirish/matchMedia.js.
Iwanttoloadthepolyfillonlyifthebrowserdoesn’tsupportthematchMediafeaturenatively.To
arrangethis,IhaveusedtheModernizr.loadmethod,whichisaflexibleresourceloader.Ipasstheload
methodanobjectwhosepropertiestellModernizrwhattodo.
TipTheModernizr.loadfeatureisavailableonlywhenyoucreateacustomModernizrbuild;itisnotincluded
intheuncompresseddevelopmentversionoftheModernizrlibrary.TheModernizrloadmethodisawrapper
aroundalibrarycalledYepNope,whichisavailableathttp://yepnopejs.com.YoucanuseYepNopedirectlyif
youdon’twanttouseacompressedModernizrbuildforanyreason.Thehttp://yepnopejs.comsitealso
containsdetailsofalloftheloaderfeatures;thesyntaxdoesn’tchangewhenthelibraryisincludedwith
Modernizr.BecarefulwhenusingaresourceloaderinexternalJavaScriptfiles.Thereareseriousissuesthatcan
arise,whichIdescribeinChapter9.YouwillseealinktocreateacustomdownloadontheModernizrwebpage.
ForthecustombuildthatIusedinthischapter,IsimplycheckedalloftheoptionstoincludeasmuchModernizr
functionalityaspossibleinthedownload.
Thetestproperty,asthenamesuggests,specifiestheexpressionthatIwantModernizrtoevaluate.
Inthiscase,Iwanttoseewhetherthewindow.matchMediamethodisdefinedbythebrowser.Youcanuse
anyJavaScriptexpressionwiththetestproperty,includingModernizrfeaturedetectionchecks.
175
www.it-ebooks.info
CHAPTER7CREATINGRESPONSIVEWEBAPPS
ThenopepropertytellsModernizrwhatresourcesIwanttoloadiftestevaluatesfalse.Inthis
example,IhavespecifiedthematchMedia.jsfile,whichcontainsthepolyfillcode.Thereisa
correspondingproperty,yep,whichtellsModernizrwhatresourcesarerequirediftestistrue,butI
don’tneedtousethatinthisexamplebecauseIwillberelyingonthebuilt-insupportformatchMediaif
testistrue.Thecompletepropertyspecifiesafunctionthatwillbeexecutedwhentheresources
specifiedbytheyepornopepropertyhaveallbeenloadedandexecuted.
Modernizr.loadgetsandexecutesJavaScriptscriptsasynchronously,whichiswhythe
detectDeviceFeaturesfunctiontakesacallbackfunctionasanargument.Iinvokethiscallbackatthe
endofthecompletefunction,passinginanobjectthatcontainsdetailsofthefeaturesthathavebeen
detected.
DetectingtheScreenSize
Icannowturntoworkingoutwhetherthedevice’sscreenfallsintomylargeorsmallcategory.Todo
this,Ipassamediaquery,justtheliketheoneIusedinCSS,tothematchMediamethod,likethis:
var screenQuery = window.matchMedia('screen AND (max-width:500px)');
Ideterminewhethermymediaqueryhasbeenmatchedbyreadingthematchespropertyofthe
objectIgetbackfrommatchMedia.Ifmatchesistrue,thenIamdealingwithascreenthatisinmysmall
category(500pixelsandsmaller).Ifitisfalse,thenIhavealargescreen.Iassigntheresulttoan
observabledataitemintheobjectthatIpasstothecallbackfunction:
var deviceConfig = {
smallScreen: ko.observable(screenQuery.matches)
};
IfthebrowserimplementsthematchMediafeature,thenIcanusetheaddListenermethodtobe
notifiedwhenthestatusofthemediaquerychanges,likethis:
if (screenQuery.addListener) {
screenQuery.addListener(function(mq) {
deviceConfig.smallScreen(mq.matches);
});
}
Thestatusofamediaquerychangeswhenoneoftheconditionsitcontainschanges.Thetwo
conditionsinmyqueryarethatweareworkingonascreenandthatithasamaximumwidthof500
pixels.Achangenotification,therefore,indicatesthatthewidthofthedisplayhaschanged.Thismeans
thatthebrowserwindowhasbeenresizedorthatthescreenorientationhaschanged(seethe
“RespondingtoScreenOrientation”sectionlaterinthischapterformoredetails).
ThematchMedia.jspolyfilldoesn’tsupportchangenotifications,soIhavetotestfortheexistenceof
theaddListenermethodbeforeIuseit.Myfunctionisexecutedwhenthestatusofthemediaquery
changesandIupdatethevalueoftheobservabledataitem.ThelastthingIdoiscreateacomputed
observabledataitem,likethis:
deviceConfig.largeScreen = ko.computed(function() {
return !deviceConfig.smallScreen();
});
ThisisjusttohelptidyupmysyntaxwhenIwanttorefertothescreensizeintherestofmyweb
appsothatIcanrefertosmalllScreenandlargeScreentofigureoutwhatIamworkingwith,asopposed
tosmallScreenand!smallScreen.Itisasmallthing,butIcreatefewertyposthisway.
176
www.it-ebooks.info
CHAPTER7CREATINGRESPONSIVEWEBAPPS
Somebrowsersareinconsistentinthewaythatstatuschangesinmediaqueriesarehandled.For
example,theversionofGoogleChromethatiscurrentasIwritethisdoesn’talwaysupdatemedia
querieswhenthescreensizechanges.Asabelt-and-bracesmeasure,Ihaveaddedasimplecheckonthe
screensize,whichissetupusingthesetIntervalfunction:
setInterval(function() {
deviceConfig.smallScreen(window.innerWidth <= 500);
}, 500);
Thefunctionisexecutedevery500millisecondsandupdatesthescreensizeitemintheviewmodel.
Thisisn’tideal,butitisimportantthataresponsivewebappisabletoadapttodevicechanges,andthis
canmeantakingsomeundesirableprecautions,includingpollingforstatuschanges.
TipNoticethatIusethewindow.innerWidthpropertytotrytofigureoutthesizeofthescreen.TheproblemI
amworkingaroundisthatthemediaqueriesdon’tworkproperlyinallbrowsers,soIneedtofindasubstitute
mechanismforassessingscreensize.
IntegratingCapabilityDetectionintotheWebApp
IwanttodetectthecapabilitiesofthedevicebeforeIdoanythingelseinthewebapp,whichIwhyI
addedacallbacktothedetectDeviceFeaturesfunction.YoucanseehowIhaveintegratedtheuseofthis
functiontothewebappscriptelementinListing7-5.
Listing7-5.CallingthedetectDeviceFeaturesFunctionfromtheInlinescriptElement
<script>
var cheeseModel = {};
detectDeviceFeatures(function(deviceConfig) {
cheeseModel.device = deviceConfig;
$.getJSON("products.json", function(data) {
cheeseModel.products = data;
}).success(function() {
$(document).ready(function() {
$('#buttonDiv input:submit').button().css("font-family", "Yanone");
$('div.cheesegroup').not("#basket").css("width", "50%");
$('div.navSelectors').buttonset();
enhanceViewModel();
ko.applyBindings(cheeseModel);
hasher.initialized.add(crossroads.parse, crossroads);
hasher.changed.add(crossroads.parse, crossroads);
hasher.init();
crossroads.addRoute("category/:newCat:", function(newCat) {
cheeseModel.selectedCategory(newCat ?
177
www.it-ebooks.info
CHAPTER7CREATINGRESPONSIVEWEBAPPS
});
});
newCat : cheeseModel.products[0].category);
});
});
</script>
IassigntheobjectthatthedetectDeviceFeaturesfunctionpassestothecallbacktothedevice
propertyintheviewmodel.Byusinganobservabledataitem,Idisseminatechangesintotheapplication
fromtheviewmodelwhenthemediaquerychanges.
Thelaststepistotakeadvantageoftheenhancementstotheviewmodelinthewebappmarkup.
Listing7-6showshowIcancontrolthevisibilityoftheCheeseLuxlogothroughadatabinding.
Listing7-6.ControllingElementVisibilityBasedonScreenCapabilityExpressedThroughtheViewModel
...
<div id="logobar" data-bind="visible: device.largeScreen()">
<img src="cheeselux.png">
<span id="tagline">Gourmet European Cheese</span>
</div>
...
Theresultistore-createtheeffectofusingtheCSSmediaqueryinJavaScript.TheCheeseLuxlogo
isvisibleonlyonlargescreens.YoumightbewonderingwhyIhavegonetoalltheeffortofre-creatinga
simpleandelegantCSStechniqueinJavaScript.Thereasonissimple:pushinginformationaboutthe
capabilitiesofthedevicethroughmywebappviewmodelgivesmeafoundationforcreatingresponsive
webappsthatarefarmorecapableandflexiblethanwouldbepossiblewithCSSalone.Thefollowing
sectiongivesanexample.
DeferringImageLoading
Theproblemwithsimplyhidinganimgelementisthatthebrowserstillloadsit;itjustnevershowsitto
theuser.Thisisaridiculoussituationbecauseitiscostingmeandtheuserbandwidthtodownloada
resourcethatwon’teverbeshownonadevicewithasmallscreen.Tofixthis,Ihavedefinedanewdata
bindingcalledifAttrintheutils.jsfile,asshowninListing7-7.Thisbindingaddsandremovesan
attributebasedonevaluatingacondition.
Listing7-7.ADataBindingforConditionallySettinganelementAttribute
ko.bindingHandlers.ifAttr = {
update: function(element, accessor) {
if (accessor().test) {
$(element).attr(accessor().attr, accessor().value);
} else {
$(element).removeAttr(accessor().attr);
}
}
}
Thisbindingexpectsadataobjectthatcontainsthreeproperties:theattrpropertyspecifieswhich
attributeIwanttoapply,thetestpropertydetermineswhethertheattributeisaddedtotheelement,
178
www.it-ebooks.info
CHAPTER7CREATINGRESPONSIVEWEBAPPS
andthevalueattributespecifiesthevaluethatwillbeassignedtotheattributeiftestistrue.Listing7-8
showshowIcanapplythisbindingtomyCheeseLuxlogomarkuptodeferloadingtheimageuntilitis
required.
Listing7-8.UsingtheifAttrBindingtoPreventImageLoading
<div id="logobar" data-bind="visible: device.largeScreen()">
<img data-bind="ifAttr: {attr: 'src', value: 'cheeselux.png',
test: device.largeScreen()}">
<span id="tagline">Gourmet European Cheese</span>
</div>
Thebrowsercan’tloadanimagewhentheimgelementdoesn’thaveasrcattribute.Totake
advantageofthis,IusetheifAttrattributewiththelargeScreenviewmodelitemsothatthesrc
attributeissetonlywhentheimagewillbedisplayed.Inthisway,Iamabletopreventtheimagefrom
loadingunlessitwillbeshown.Thisisaprettysimpletrickbutdemonstratesthekindofflexibilitythat
youshouldlookforwhencreatingaresponsivewebapp.
TipItisimportanttodistinguishbetweenresourcesthatyoudon’twanttouseimmediatelyfromresourcesthat
youareunlikelytowantatall.Ifyouhaveareasonableexpectationthattheuserwillrequireanimageinthe
normaluseofyourapplication,thenyoushouldletthebrowserdownloaditsothatitisimmediatelyavailable
whenrequired.UsetheifAttrtechniquetoavoidawasteddownloadifitisunlikelythattheuserwillrequirea
resource.
AdaptingtheWebAppLayout
Fromthispointon,IsimplyhavetoadapteachpartofthewebapptothetwocategoriesofscreenthatI
aminterestedin.Listing7-9showsthechangesthatarerequired.
TipDon’ttrytoloadthislistinginthebrowseruntilyouhavealsoappliedthechangesinListing7-10.Ifyoudo,
you’llgetanerrorbecausetheviewmodeldataandthedatabindingsareoutofsync.
Listing7-9.AdaptingtheWebApptoLargeandSmallScreens
<!DOCTYPE html>
<html>
<head>
<title>CheeseLux</title>
<link rel="stylesheet" type="text/css" href="styles.css"/>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script src="jquery-ui-1.8.16.custom.js" type="text/javascript"></script>
179
www.it-ebooks.info
CHAPTER7CREATINGRESPONSIVEWEBAPPS
<script src='knockout-2.0.0.js' type='text/javascript'></script>
<script src='utils.js' type='text/javascript'></script>
<script src='signals.js' type='text/javascript'></script>
<script src='crossroads.js' type='text/javascript'></script>
<script src='hasher.js' type='text/javascript'></script>
<script src='modernizr-2.0.6.js' type='text/javascript'></script>
<link rel="stylesheet" type="text/css" href="jquery-ui-1.8.16.custom.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script>
var cheeseModel = {};
detectDeviceFeatures(function(deviceConfig) {
cheeseModel.device = deviceConfig;
$.getJSON("products.json", function(data) {
cheeseModel.products = data;
}).success(function() {
$(document).ready(function() {
function performScreenSetup(smallScreen) {
$('div.cheesegroup').not("#basket")
.css("width", smallScreen ? "" : "50%");
};
cheeseModel.device.smallScreen.subscribe(performScreenSetup);
performScreenSetup(cheeseModel.device.smallScreen());
$('div.buttonDiv input:submit').button();
$('div.navSelectors').buttonset();
enhanceViewModel();
ko.applyBindings(cheeseModel);
hasher.initialized.add(crossroads.parse, crossroads);
hasher.changed.add(crossroads.parse, crossroads);
hasher.init();
});
crossroads.addRoute("category/:newCat:", function(newCat) {
cheeseModel.selectedCategory(newCat ?
newCat : cheeseModel.products[0].category);
});
crossroads.parse(location.hash.slice(1));
});
});
</script>
</head>
<body>
<div id="logobar" data-bind="visible: device.largeScreen()">
<img data-bind="ifAttr: {attr: 'src', value: 'cheeselux.png',
test: device.largeScreen()}">
<span id="tagline">Gourmet European Cheese</span>
</div>
180
www.it-ebooks.info
CHAPTER7CREATINGRESPONSIVEWEBAPPS
<div class="cheesegroup">
<div class="navSelectors" data-bind="foreach: products">
<a data-bind="formatAttr: {attr: 'href', prefix: '#category/',
value: category},
css: {selectedItem: (category == cheeseModel.selectedCategory())}">
<span data-bind="text: cheeseModel.device.smallScreen() ?
shortName : category"></span>
</a>
</div>
</div>
<div id="basket" class="cheesegroup basket"
data-bind="visible: cheeseModel.device.largeScreen()">
<div class="grouptitle">Basket</div>
<div class="groupcontent">
<div class="description" data-bind="ifnot: total">
No products selected
</div>
<table id="basketTable" data-bind="visible: total">
<thead><tr><th>Cheese</th><th>Subtotal</th><th></th></tr></thead>
<tbody data-bind="foreach: products">
<!-- ko foreach: items -->
<tr data-bind="visible: quantity, attr: {'data-prodId': id}">
<td data-bind="text: name"></td>
<td>$<span data-bind="text: subtotal"></span></td>
</tr>
<!-- /ko -->
</tbody>
<tfoot>
<tr><td class="sumline" colspan=2></td></tr>
<tr>
<th>Total:</th><td>$<span data-bind="text: total"></span></td>
</tr>
</tfoot>
</table>
</div>
<div class="cornerplaceholder"></div>
<div class="buttonDiv">
<input type="submit" value="Submit Order"/>
</div>
</div>
<form action="/shipping" method="post">
<div data-bind="foreach: products">
<div class="cheesegroup"
data-bind="fadeVisible: category == $root.selectedCategory()">
<div class="grouptitle" data-bind="text: category"></div>
<!-- ko foreach: items -->
<div class="groupcontent">
181
www.it-ebooks.info
CHAPTER7CREATINGRESPONSIVEWEBAPPS
<label data-bind="attr: {for: id}" class="cheesename">
<span data-bind="text: name">
</span> $(<span data-bind="text:price"></span>)</label>
<input data-bind="attr: {name: id}, value: quantity"/>
<span data-bind="visible: subtotal" class="subtotal">
($<span data-bind="text: subtotal"></span>)
</span>
</div>
<!-- /ko -->
<div class="groupcontent" data-bind="if: $root.device.smallScreen()">
<label class="cheesename">Total:</label>
<span class="subtotal" id="total">
$<span data-bind="text: cheeseModel.total()"></span>
</span>
</div>
</div>
</div>
<div class="buttonDiv" data-bind="visible: $root.device.smallScreen()">
<input type="submit" value="Submit Order"/>
</div>
</form>
</body>
</html>
Thejoyofthisapproachishowfewchangesarerequiredtomakeawebappresponsivetoscreen
size(andhowsimplethosechangesare).Thatsaid,thereareasmallnumberofchangesthatrequire
explanation,whichIprovideinthefollowingsections.Youcanseehowmyresponsivewebappappears
onlargeandsmallscreensinFigure7-4.
Figure7-4.Thesamewebappdisplayedonalargeandsmallscreen
182
www.it-ebooks.info
CHAPTER7CREATINGRESPONSIVEWEBAPPS
Thesesmallchangeshaveabigimpact,andforthemostpart,thechangesarecosmetic.The
underlyingfeaturesandstructureofmywebappremainthesame.Idon’thavetoforgomyviewmodel
orroutingjusttosupportadevicewithasmallerscreen.
AdaptingtheSourceData
Thecategorybuttonsareaproblemonasmallscreen,soIwanttodisplaysomethingtotheuserthatis
meaningfulbutrequireslessscreenspace.Todothis,Imadesomeadditionstotheproducts.jsonfileso
thateachcategorycontainsanametobeusedwhenspaceislimited.Listing7-10showstheadditionfor
oneofthecategories.
Listing7-10.AddingScreen-SpecificInformationtotheProductData
...
[{"category": "British Cheese",
"shortName": "British",
"items" : [
{"id": "stilton", "name": "Stilton", "price": 9,
"description": "A semi-soft blue cow's milk cheese produced in the
Nottinghamshire region. A strong cheese with a distinctive smell
and taste and crumbly texture."},
...
Ihaveappliedasimilarchangetoalloftheothercategoriesintheproducts.jsonfile.Icouldhave
arrivedattheshortnamebysplittingthecategoryvaluestringonthespacecharacter,butIwantto
makethepointthatitisnotjustthescriptandmarkupinawebappthatcanberesponsive;youcanalso
supportthisconceptinthedatathatdrivesyourapplication.
InListing7-9,Imodifiedthedatabindingforthenavigationbuttonstotakeadvantageofthe
shortercategoriesnames,likethis:
<div class="cheesegroup">
<div class="navSelectors" data-bind="foreach: products">
<a data-bind="formatAttr: {attr: 'href', prefix: '#category/',
value: category},
css: {selectedItem: (category == cheeseModel.selectedCategory())}">
<span data-bind="text: cheeseModel.device.smallScreen() ?
shortName : category"></span>
</a>
</div>
</div>
IstillusethefullcategorynamefortheformatAttrbinding.Thisallowsmetousethesamesetof
navigationroutesirrespectiveofthescreensize(seeChapter4fordetailsofusingroutinginawebapp).
ApplyingConditionaljQueryUIStyling
Inthelargescreenlayout,Iresizetheproductlistelementstomakeroomforthebasket.Inthesmall
screenlayout,Ireplacethededicatedbasketwithaone-linetotalattheendofeachsection.Iliketotake
advantageofthematchMedia.addListenerfeatureifitisavailable,whichmeansImustbeabletotoggle
betweenthesmallandlargescreenlayoutsasneeded.Toaccommodatethis,Itreatthosescript
183
www.it-ebooks.info
CHAPTER7CREATINGRESPONSIVEWEBAPPS
statementsthatdrivetheindividuallayoutsintheirownfunctionandregisterthatfunctionasa
subscribertochangesintheviewmodel:
function performScreenSetup(smallScreen) {
$('div.cheesegroup').not("#basket").css("width", smallScreen ? "" : "50%");
};
cheeseModel.device.smallScreen.subscribe(performScreenSetup);
Thefunctionwillbecalledonlywhenthevaluechanges,soIcallthefunctionexplicitlytogetthe
rightbehaviorwhenthedocumentisfirstloaded,likethis:
performScreenSetup(cheeseModel.device.smallScreen());
Ineffect,ItoggletheCSSwidthpropertyofthedivelementsinthecheesegroupclassbasedonthe
sizeofthescreen.Youcouldignorethisapproachandjustleavethelayoutinitsinitialstate,butIthink
thatisalostopportunitytoprovideaniceexperiencefordesktopusers.
RemovingElementsfromtheDocument
Forthemostpart,Isimplyhideandshowelementsinthedocumentbasedonthesizeofthescreen.
However,thereareoccasionswhentheifandifnotbindingsarerequiredtoensurethatelementsare
completelyremovedfromthedocument.AsimpleexampleofthiscanbeseeninthelistingwhereIuse
theifbindingfortheone-linetotalsummary:
<div class="groupcontent" data-bind="if: $root.device.smallScreen()">
<label class="cheesename">Total:</label>
<span class="subtotal" id="total">
$<span data-bind="text: cheeseModel.total()"></span>
</span>
</div>
Ihaveusedtheifbindingherebecausetuckedawayinthestyles.cssfileisaCSSstylethatapplies
roundedcorners:
div.groupcontent:last-child {
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
}
Thebrowserdoesn’ttakeintoaccountthevisibilityofelementswhenworkingoutwhichisthelast
childofitsparent.IfIhadusedthevisiblebinding,thenIdon’tgettheroundedcornersIwantinthe
largescreenlayout.TheifbindingforcesthebehaviorIwantbyremovingtheelementsentirely,
ensuringthattheroundedcornersareappliedcorrectly.
RespondingtoScreenOrientation
Manymobiledevicesrespondtothewaythattheuserisholdingthedevicebychangingthescreen
orientationbetweenlandscapeandportraitmodes.Keepinginformedofthedisplaymodeturnsoutto
bequitetricky,butitisworthdoingtomakesurethatyourwebapprespondsappropriatelywhenthe
orientationchanges.Thereareseveralwaystoapproachthisissue.
Somedevicessupportawindow.orientationpropertyandanorientationchangeeventtomakeit
easiertokeeptrackofthescreenorientation,butthisfeatureisn’tuniversal,andevenwhenitis
implemented,theeventtendstobefiredwhenitshouldn’tbe(andisn’tfiredwhenitshouldbe).
184
www.it-ebooks.info
CHAPTER7CREATINGRESPONSIVEWEBAPPS
Otherdevicessupportorientationaspartofamediaquery.ThisisusefuliftheaddListenerfeature
issupportedaspartofmatchMedia,butmostmobilebrowsersdon’tsupportthisfeature,andtheseare
thedeviceswhoseorientationismostlikelytochange.
Almostallbrowserssupportaresizeevent,whichistriggeredwhenthewindowisresizedorthe
orientationischanged.However,someimplementationsintroducedelaysbetweenorientationchanges
andtheeventbeingtriggered,whichmakesforawebappthatisslowtorespondandthatmaychange
itslayoutorbehavioraftertheuserhasstartedinteractingintheneworientation.
Thefinalapproachistoperiodicallycheckscreendimensionsandworkouttheorientation
manually.Thisiscrudebuteffectiveandworksonlyifthefrequencyofthecheckishighenoughtomake
forarapidresponsebutlowenoughnottooverwhelmthedevice.
Theonlyreliablewaytomakesureyoudetectorientationchangesistoapplyallfourtechniques.
Listing7-11showstherequiredadditionstothedetectDeviceFeaturesfunction.
Listing7-11.DetectingScreenOrientationChanges
function detectDeviceFeatures(callback) {
var deviceConfig = {};
deviceConfig.landscape = ko.observable();
deviceConfig.portrait = ko.computed(function() {
return !deviceConfig.landscape();
});
var setOrientation = function() {
deviceConfig.landscape(window.innerWidth > window.innerHeight);
}
setOrientation();
$(window).bind("orientationchange resize", function() {
setOrientation();
});
setInterval(setOrientation, 500);
if (window.matchMedia) {
var orientQuery = window.matchMedia('screen AND (orientation:landscape)')
if (orientQuery.addListener) {
orientQuery.addListener(setOrientation);
}
}
Modernizr.load({
test: window.matchMedia,
nope: 'matchMedia.js',
complete: function() {
var screenQuery = window.matchMedia('screen AND (max-width:500px)');
deviceConfig.smallScreen = ko.observable(screenQuery.matches);
if (screenQuery.addListener) {
screenQuery.addListener(function(mq) {
deviceConfig.smallScreen(mq.matches);
});
185
www.it-ebooks.info
CHAPTER7CREATINGRESPONSIVEWEBAPPS
}
deviceConfig.largeScreen = ko.computed(function() {
return !deviceConfig.smallScreen();
});
setInterval(function() {
deviceConfig.smallScreen(window.innerWidth <= 500);
}, 500);
}
};
callback(deviceConfig);
});
Ihavesetuptwoviewmodeldataitems,landscapeandportrait,followingthesamepatternthatI
usedforsmallScreenandlargeScreen.Idon’twanttoduplicatemycodefortestingtheorientationof
thedevice,soIhavecreatedasimpleinlinefunctioncalledsetOrientationthatsetsthevalueofthe
landscapedataitem:
var setOrientation = function() {
deviceConfig.landscape(window.innerWidth > window.innerHeight);
}
IhavefoundcomparingtheinnerWidthandinnerHeightvaluesofthewindowobjecttobethemost
reliablewayoffiguringoutthescreenorientation.Thescreen.widthandscreen.heightvaluesshould
work,butsomebrowsersdon’tchangethesevalueswhenthedeviceisreoriented.The
window.orientationpropertyprovidesgoodinformation,butitisn’tuniversallyimplemented.Thisisan
undoubtedcompromise,andIrecommendyoutesttheefficacyofthisapproachonyourtargetdevices.
TherestoftheadditionsimplementthevariousmeansbywhichthesetOrientationwillbecalled:
viatheorientationchangeandresizeevents,viaamediaquery,andviapolling.Judgingtheright
frequencytopolltheorientationisdifficult,butIusuallyuse500milliseconds.Itisn’talwaysas
responsiveasIwouldlike,butitstrikesareasonablebalance.
TipIcouldhaveusedasinglesetIntervalcalltopollforboththescreensizeandtheorientation,butIprefer
tokeeptheregionsofcodefunctionalityasseparateaspossible.
IntegratingScreenOrientationintotheWebApp
Icanmakethewebapprespondtothescreenorientationnowthattheviewmodelhastheportraitand
landscapeitems.Todemonstratethis,Iamgoingtofixaproblem:thewebappcurrentlyrequiresthe
usertoscrolldowntoseealloftheelementsinlandscapemodeonadevicethathasasmallscreen.
Figure7-5showstheproblemandtheresultafterIhavemodifiedthewebapplayout.
186
www.it-ebooks.info
CHAPTER7CREATINGRESPONSIVEWEBAPPS
Figure7-5.Respondingtothelandscapeorientationonsmallscreens
Torespondtothisorientationforsmallscreens,Ihaveremovedthecategorynavigationelements
andreplacedthemwithleftandrightbuttonsthatpagethroughthecategories.Thisisn’tthemost
elegantapproach,butitmakesgooduseoflimitedscreenspacewhilepreservingthebasicnatureofthe
webapp.Listing7-12showstheadditionofthedatabindingtocontrolvisibilityforthenavigationitems.
Listing7-12.BindingElementVisibilitytotheScreenSizeandOrientation
<div class="cheesegroup"
data-bind="ifnot: cheeseModel.device.smallScreen() &&
cheeseModel.device.landscape()">
<div class="navSelectors" data-bind="foreach: products">
<a data-bind="formatAttr: {attr: 'href', prefix: '#category/',
value: category},
css: {selectedItem: (category == cheeseModel.selectedCategory())}">
<span data-bind="text: cheeseModel.device.smallScreen()?
shortName : category"></span>
</a>
</div>
</div>
IremovetheelementsfromtheDOMifthedevicehasasmallscreenandisinthelandscape
orientation.ThebuttonsIaddareasfollows:
<div class="buttonDiv" data-bind="visible: $root.device.smallScreen()">
<button id="left">Prev</button>
<input type="submit" value="Submit Order"/>
<button id="right">Next</button>
</div>
Theelementsthemselvesarenotinteresting,butthecodethathandlesthenavigationthatarises
whenclickedisworthlookingat:
187
www.it-ebooks.info
5
CHAPTER7CREATINGRESPONSIVEWEBAPPS
...
function performScreenSetup(smallScreen) {
$('div.cheesegroup').not("#basket")
.css("width", smallScreen ? "" : "50%");
$('button#left').button({icons:
{primary: "ui-icon-circle-triangle-w"},text: false});
$('button#right').button({icons:
{primary: "ui-icon-circle-triangle-e"},text: false});
$('button#left, button#right').click(function(e) {
e.preventDefault();
advanceCategory(e, this.id);
});
};
...
Thisisanexampleofwhenusingroutingfornavigationdoesn’twork.Iwanttheusertobeableto
repeatedlyclickthesebuttons,andasImentionedalready,thebrowserwon’trespondtoanattemptto
navigatetothesameURLthatisalreadybeingdisplayed.Withthisinmind,IhaveusedthejQueryclick
methodtohandletheregularJavaScripteventbycallingtheadvanceCategoryfunction.Idefinedthis
functioninutils.js,anditisshowninListing7-13.
Listing7-13.TheadvanceCategoryFunction
function advanceCategory(e, dir) {
var cIndex = -1;
for (var i = 0; i < cheeseModel.products.length; i++) {
if (cheeseModel.products[i].category == cheeseModel.selectedCategory()) {
cIndex = i;
break;
}
}
cIndex = (dir == "left" ? cIndex - 1 : cIndex + 1) % (cheeseModel.products.length);
if (cIndex < 0) {
cIndex = cheeseModel.products.length -1;
}
cheeseModel.selectedCategory(cheeseModel.products[cIndex].category)
}
Thereisnoneatorderingofcategoriesintheviewmodel,soIenumeratethroughthedatatofind
theindexofthecurrentlyselectedcategoryandincrementordecrementthevaluebasedonwhich
buttonhasbeenclicked.Theresultisamorecompactlayoutthatbettersuitsthesmall-screen
landscapeorientation.ThewayIhavecategorizeddevicesisprettycrude,andIrecommendyoutakea
moregranularapproachinrealprojects,butitservestodemonstratethetechniquesyouneedinorder
torespondtoscreenorientation.
RespondingtoTouch
Thefinalfeaturethataresponsivewebappneedstodealwithistouchsupport.Theideaoftouch-based
interactionisfirmlyestablishedinthesmartphoneandtabletmarkets,butitisalsomakingitswaytothe
desktop,mostlythroughMicrosoftWindows8.
188
www.it-ebooks.info
CHAPTER7CREATINGRESPONSIVEWEBAPPS
Tosupporttouchinteraction,weneedtwothings:atouchscreenandabrowserthatemitstouch
events.Thesetwodon’talwayscometogether;pluggingatouch-enabledmonitorintoadesktop
machinedoesn’tautomaticallyenabletouchinthebrowser,forexample.Equally,youshouldnot
assumethatifadevicesupportstouchthatthiswillbetheonlymodelforinteractions.Manydeviceswill
supportmouseandkeyboardinteractionsalongsidetouch,andtheusershouldbeabletopick
whichevermodelsuitsthemwhenusingyourwebappandswitchfreelybetweenthem.
Devicesthatdon’thavearegularmouseandkeyboardsynthesizeeventssuchasclickinresponse
totouchevents.Thismeansyoudon’tneedtomakechangestoyourwebapptosupportbasictouch
interactions.However,tocreateatrulyresponsewebapp,youshouldconsidersupportingthe
navigationgesturesthatarecommonontouchdevices,suchasswiping.Idemonstratehowtodothis
shortly.
DetectingTouchSupport
ThereisaW3Cspecificationfortouchevents,butitislow-level,andalotofworkisrequiredtofigure
outwhatgesturestheuserismaking.AsIhavesaidbefore,partofthejoyofwebappdevelopmentisthe
availabilityofhigh-qualityJavaScriptlibrariesthatmakedevelopmentsimpler.Onesuchexampleis
touchSwipe,whichbuildsonjQueryandtransformsthelow-leveltoucheventsintoeventsthatrepresent
gestures.IincludedthetouchSwipelibraryinthesourcecodedownloadthataccompaniesthisbookand
thatisavailablefromApress.com.Thewebsiteforthelibraryishttp://labs.skinkers.com/touchSwipe.
ThesimplestandmostreliableapproachtodetectingtouchsupportistorelyontheModernizrtest.
Listing7-14showstheadditionstothedetectDeviceFeaturesfunctionintheutils.jsfiletodetectand
reportontouchsupportandshowstheuseoftouchSwipetorespondtotouchevents.
Listing7-14.DetectingSupportforTouchEvents
function detectDeviceFeatures(callback) {
var deviceConfig = {};
deviceConfig.landscape = ko.observable();
deviceConfig.portrait = ko.computed(function() {
return !deviceConfig.landscape();
});
var setOrientation = function() {
deviceConfig.landscape(window.innerWidth > window.innerHeight);
}
setOrientation();
$(window).bind("orientationchange resize", function() {
setOrientation();
});
setInterval(setOrientation, 500);
if (window.matchMedia) {
var orientQuery = window.matchMedia('screen AND (orientation:landscape)')
if (orientQuery.addListener) {
orientQuery.addListener(setOrientation);
}
}
189
www.it-ebooks.info
CHAPTER7CREATINGRESPONSIVEWEBAPPS
};
Modernizr.load([{
test: window.matchMedia,
nope: 'matchMedia.js',
complete: function() {
var screenQuery = window.matchMedia('screen AND (max-width:500px)');
deviceConfig.smallScreen = ko.observable(screenQuery.matches);
if (screenQuery.addListener) {
screenQuery.addListener(function(mq) {
deviceConfig.smallScreen(mq.matches);
});
}
deviceConfig.largeScreen = ko.computed(function() {
return !deviceConfig.smallScreen();
});
}
}, {
test: Modernizr.touch,
yep: 'jquery.touchSwipe-1.2.5.js',
callback: function() {
$('html').swipe({
swipeLeft: advanceCategory,
swipeRight: advanceCategory
});
}
},{
complete: function() {
callback(deviceConfig);
}
}]);
WhenyoupassanarrayofobjectstotheModernizr.loadmethod,eachtestisperformedinturn.I
haveaddedatestthatusestheModernizr.touchcheckandthatloadsthetouchSwipelibraryiftouch
supportispresent.
TipMakesureyouincludedthetouchtestsifyoudownloadedyourownversionofModernizr.TheversionI
includedinthesourcecodeforthischaptercontainsalloftheavailabletests.
NoticethatIusedthecallbackpropertytosetupsupportforhandlingswipes.Functionssetusing
thecallbackpropertyareexecutedwhenthespecifiedresourcesareloaded,whereasfunctionsspecified
usingcomplete areexecutedattheendofthetest,irrespectiveofthetestresult.Iwanttohandleswipe
eventsonlyiftouchSwipehasbeenloaded(whichitselfindicatesthattouchsupportispresent),soI
haveusedcallbacktogiveModernizrmyfunction.
ThetouchSwipelibraryisappliedusingtheswipemethod.Inthisexample,Ihaveselectedthehtml
elementasthetargetfordetectingswipegestures.Somebrowserslimitthebodyelementsizesothatit
doesn’tfilltheentirewindowwhenthecontentissmallerthantheavailablespace.Thisisn’tusuallya
190
www.it-ebooks.info
CHAPTER7CREATINGRESPONSIVEWEBAPPS
problem,butitcreatesdeadspotsonthescreenwhendealingwithgestures,whichmaynotbetargeted
atindividualelements.Thesimplestwaytogetaroundthisistoworkonthehtmlelement.
ThetouchSwipelibraryisabletodifferentiatebetweendifferentkindsoftoucheventsandswipesin
arangeofdirections.Icareaboutswipesonlytotheleftandtherightinthisexample,whichiswhyI
havedefinedafunctionfortheswipeLeftandswipeRightpropertiesintheobjectIpassedtotheswipe
method.InbothcasesIhavespecifiedtheadvanceCategoryfunction,whichisthesamefunctionIused
tochangeselectedcategoriesearlier.Theresultisthatswipingleftmovestothepreviouscategoryand
swipingrightgoestothenextcategory.Thelastpointtonoteaboutthislistingisthelastiteminthe
arraypassedtotheModernizr.loadmethod:
{
}
complete: function() {
callback(deviceConfig);
}
Idon’twanttoinvokethecallbackfunctionuntilIhavesetupallofthedevicedetailsintheresult
objectthatwillbeaddedtotheviewmodel.Theeasiestwaytoensurethishappensistocreatean
additionaltestthatcontainsjustacompletefunction.Modernizrwon’texecutethisfunctionuntilallof
theothertestshavebeenperformed,therequiredresourceshavebeenloaded,andthecallbackand
completefunctionsforalloftheprevioustestshavebeenperformed.
UsingTouchtoNavigatetheWebAppHistory
Inthepreviousexample,Irespondtoswipegesturesbyloopingthroughtheavailableproduct
categories.Inthissection,Ishowyouhowtorespondtothesegesturesinamoreusefulway.
Thetemptationistousethebrowser’shistorytorespondtoswipes.Theproblemisthatthereisno
waytopeekatthepreviousornextentryinthehistoryandseewhetheritisonethatbelongstotheweb
app.Ifitisn’t,thenyouendupmakingtheusernavigateawayfromyourwebapp,potentiallytoaURL
thattheyhadnointentionofvisiting.Listing7-15showsthechangesrequiredtotheenhanceViewModel
functionintheutils.jsfiletosetupthebasicsupportfortrackingtheuser’scategoryselections.
TipYoucouldelecttouselocalstorageandmaketheswipe-relatedhistorypersistent.Iprefernottodothis,
sinceIthinkitmakesmoresenseforthehistorytobelimitedtothecurrentlifeofthewebapp.
Listing7-15.AddingApplication-SpecificHistoryUsingSessionStorage
function enhanceViewModel() {
cheeseModel.selectedCategory = ko.observable(cheeseModel.products[0].category);
mapProducts(function(item) {
item.quantity = ko.observable(0);
item.subtotal = ko.computed(function() {
return this.quantity() * this.price;
}, item);
}, cheeseModel.products, "items");
191
www.it-ebooks.info
CHAPTER7CREATINGRESPONSIVEWEBAPPS
cheeseModel.total = ko.computed(function() {
var total = 0;
mapProducts(function(elem) {
total += elem.subtotal();
}, cheeseModel.products, "items");
return total;
});
var history = cheeseModel.history = {};
history.index = 0;
history.categories = [cheeseModel.selectedCategory()];
cheeseModel.selectedCategory.subscribe(function(newValue) {
if (newValue != history.categories[history.index]) {
history.index++;
history.categories.push(newValue);
}
})
};
Theadditionsaresimple.Ihaveaddedanindexandanarraytotheviewmodelandsubscribedto
theselectedCategoryobservabledataitemsothatIcanbuilduptheuser’shistoryastheychange
categories.IhavenotworriedaboutmanagingthesizeofthearraysinceIthinkitisunlikelythatenough
categorychangeswillbemadetocauseacapacityproblem.Listing7-16showsthechangestothead.
Listing7-16.TakingAdvantageoftheApp-SpecificHistory
function advanceCategory(e, dir) {
if (cheeseModel.device.smallScreen() && cheeseModel.device.landscape()) {
var cIndex = -1;
for (var i = 0; i < cheeseModel.products.length; i++) {
if (cheeseModel.products[i].category == cheeseModel.selectedCategory()) {
cIndex = i;
break;
}
}
cIndex = (dir == "left" ? cIndex-1 : cIndex + 1) % (cheeseModel.products.length);
if (cIndex < 0) {
cIndex = cheeseModel.products.length -1;
}
cheeseModel.selectedCategory(cheeseModel.products[cIndex].category)
} else {
var history = cheeseModel.history;
if (dir == "left" && history.index > 0) {
cheeseModel.selectedCategory(history.categories[--history.index]);
} else if (dir == "right" && history.index < history.categories.length -1) {
cheeseModel.selectedCategory(history.categories[++history.index]);
}
}
}
192
www.it-ebooks.info
CHAPTER7CREATINGRESPONSIVEWEBAPPS
Ihavetobecarefulnottoapplytheswipehistorywhenthewebappisdisplayedonasmallscreen
inthelandscapeorientation.Iremovedthecategorybuttonsinthisdeviceconfiguration,meaningthat
thereisnowayfortheusertogenerateahistoryformetonavigatethrough.Inallotherdevice
configurations,Iamabletorespondtotheswipebychangingthevalueoftheindexandselectingthe
correspondinghistoriccategory.Theresultisthattheusercannavigatebetweencategoriesusingthe
navigationbuttonsandswipingmovesbackwardorforwardthroughtherecentselections.
IntegratingwiththeApplicationRoutes
ThelasttweakIwanttomakeistorespondtotheswipeeventsthroughthewebapp’sURLroutes.Inthe
lastlisting,Itooktheshortcutofchangingtheobservabledataitemdirectly,butthismeansIwillbypass
anycodethatisgeneratedasaresultofaURLchange,includingintegrationwiththeHTML5HistoryAPI
(whichIdescribeinChapter4).ThechangesareshowninListing7-17.
Listing7-17.RespondingtoSwipeEventsThroughtheApplicationRoutes
function advanceCategory(e, dir) {
if (cheeseModel.device.smallScreen() && cheeseModel.device.landscape()) {
var cIndex = -1;
for (var i = 0; i < cheeseModel.products.length; i++) {
if (cheeseModel.products[i].category == cheeseModel.selectedCategory()) {
cIndex = i;
break;
}
}
cIndex = (dir == "left" ? cIndex-1 : cIndex + 1) % (cheeseModel.products.length);
if (cIndex < 0) {
cIndex = cheeseModel.products.length -1;
}
cheeseModel.selectedCategory(cheeseModel.products[cIndex].category)
}
} else {
var history = cheeseModel.history;
if (dir == "left" && history.index > 0) {
location.href = "#category/" + history.categories[--history.index];
} else if (dir == "right" && history.index < history.categories.length -1) {
location.href = "#category/" + history.categories[++history.index];
}
}
IhaveusedthebrowserlocationobjecttochangetheURLthatthebrowserdisplays.SinceIhave
specifiedrelativeURLs,thebrowserwillnotnavigateawayfromthewebapp,andmyrouteswillbeable
tomatchtheURLs.Bydoingthis,Iensurethatmyresponsetoswipeeventsisconsistentwithother
formsofnavigation.
193
www.it-ebooks.info
CHAPTER7CREATINGRESPONSIVEWEBAPPS
Summary
Inthischapter,Ihaveshownyouthethreecharacteristicsthatyoumustadapttoinordertocreatea
responsivewebapp:screensize,screenorientation,andtouchinteraction.Bydetectingandadaptingto
differentdeviceconfigurations,youcancreateonewebappthatcanseamlesslyandelegantlyadaptits
layoutandinteractionmodeltosuittheuser’sdevice.Theadvantagesofsuchanapproachareobvious
whenyouconsidertheproliferationofsmartphonesandtabletsandtheblurringofthedistinctions
betweenthesedevicesanddesktops.Inthenextchapter,Ishowyouadifferentapproachtosupporting
differenttypesofdevices:creatingaplatform-specificwebapp.
194
www.it-ebooks.info
CHAPTER 8
Creating Mobile Web Apps
Analternativetocreatingawebappthatadaptstothecapabilitiesofdifferentdevicesistocreatea
versionthatisspecificallytargetedtomobiledevices.Choosingbetweenaresponsivewebappanda
mobile-specificimplementationcanbedifficult,butmyruleofthumbisthatamobileversionmakes
sensewhenIwanttoofferaradicallydifferentexperiencetomobileanddesktopusersorwhendealing
withdeviceconstraintsinaresponsiveimplementationbecomesunwieldyandoverlycomplex.Your
decisionwill,ofcourse,dependonthespecificsofyourproject,butthischapterisforwhenyoudecide
thatoneversionofyourwebapp,howeverresponsive,won’tcatertoyourmobileusers’needs.
DetectingMobileDevices
Thefirststepistodecidehowyouaregoingtodirectusersofmobiledevicestothemobileversionof
yourwebapp.Thedecisionyoumakeatthisstagewillshapealotoftheassumptionsyouwillhave
whenyoucometobuildthemobilewebapp.Thereareacoupleofbroadapproaches,whichIdescribe
inthefollowingsections.
DetectingtheUserAgent
Thetraditionalapproachistolookattheuseragentstringthatthebrowserusestodescribeitself.Thisis
availablethroughthenavigator.userAgentproperty,andthevaluethatitreturnscanbeusedtoidentify
thebrowserand,usually,theplatformthebrowserisrunningon.Asanexample,hereisthevalueof
navigator.userAgentthatChromereturnsonmyWindowssystem:
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.77
Safari/535.7"
And,forcontrast,hereiswhatIgetfromtheOperaMobileemulator:
Opera/9.80 (Windows NT 6.1; Opera Mobi/23731; U; en) Presto/2.9.201 Version/11.50"
Youcanidentifymobiledevicesbybuildingalistofuseragentvaluesandkeepingtrackofwhich
onesrepresentmobilebrowsers.Youdon’thavetocreateandmanagetheselistsyourself,however—
therearesomegoodsourcesofinformationavailableonline.(Averycomprehensivedatabasecalled
WURFLcanbefoundathttp://wurfl.sourceforge.net,butthisrequiresintegrationintoyourserversidecode,whichisnotidealforthisbook.)
Aless-comprehensiveclient-sidesolutioncanbefoundathttp://detectmobilebrowsers.com,where
youcandownloadasmalljQuerylibrarythatmatchestheuseragentagainstaknownlistofmobile
browsers.Thisapproachisn’tascompleteasWURFL,butitissimplertouse,anditdetectsthemost
widelyusedmobilebrowsers.Todemonstratethiskindofmobiledevicedetection,Idownloadedthe
jQuerycodetomyNode.jscontentdirectoryinafilecalleddetectmobilebrowser.js(youcanfindthis
195
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
fileinthesourcecodedownloadforthisbook,availablefromApress.com).Listing8-1showshowtouse
thisplugintodetectmobiledevices.
Listing8-1.DetectingMobileDevicesattheClient
<!DOCTYPE html>
<html>
<head>
<title>CheeseLux</title>
<link rel="stylesheet" type="text/css" href="styles.css"/>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script src="jquery-ui-1.8.16.custom.js" type="text/javascript"></script>
<script src='knockout-2.0.0.js' type='text/javascript'></script>
<script src='utils.js' type='text/javascript'></script>
<script src='signals.js' type='text/javascript'></script>
<script src='crossroads.js' type='text/javascript'></script>
<script src='hasher.js' type='text/javascript'></script>
<script src='modernizr-2.0.6.js' type='text/javascript'></script>
<script src='detectmobilebrowser.js' type='text/javascript'></script>
<link rel="stylesheet" type="text/css" href="jquery-ui-1.8.16.custom.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script>
if ($.browser.mobile) {
location.href = "mobile.html";
}
var cheeseModel = {};
...
OnceIhaveaddedthelibrarytomydocumentwithascriptelement,Icanchecktoseewhethermy
webappisrunningonamobilebrowserbyreadingthe$.browser.mobileproperty,whichreturnstrueif
theuseragentisrecognizedasbelongingtoamobilebrowser.Inthiscase,Iredirectmobileuserstothe
mobile.htmldocument,whichIwillusetobuildmymobilewebapplaterinthischapter.
Themainproblemwithusingtheuseragentisthatitisn’talwaysaccurate,andasImentionedin
thepreviouschapter,thedistinctionsbetweenmobileanddesktopdevicesarebecomingblurred.In
essence,yourelyonsomeoneelse’sdecisionaboutwhatdefinesmobile,andthatwon’talwayslineup
withthewayyouwanttosegmentyouruserbase.And,althoughthelistsofbrowseraregenerally
accurate,itcantakeawhilefornewmodelstobeproperlyidentifiedandcategorized,especiallyfrom
nichehardwareproviders.
Arelatedproblemisthatmanybrowsersallowtheusertochangetheuseragentsothatanother
browserisidentified.Notmanyusersmakethischange,butitdoesmeanyoucannotentirelyrelyonthe
useragentreportedthroughthenavigator.userAgentproperty.
DetectingDeviceCapabilities
Iprefertoclassifyadeviceasmobilebydetectingitscapabilities,muchasIdidinChapter7.Thisallows
metodecidewhatdefinesmobileinthecontextofthewaymywebappworks.FortheCheeseLuxweb
app,Ihavedecidedthatdevicesthataretouchenabledandthathavescreensthatarenarrowerthan500
pixelswillbegiventhemobileversionofmywebapp.YoucanseehowIhaveimplementedthispolicy
inListing8-2,whichshowsthechangestothedetectDeviceFeaturesfunctionfromtheutils.jsfile.
196
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
Listing8-2.DetectingMobileDevicesBasedonTheirCapabilities
function detectDeviceFeatures(callback) {
var deviceConfig = {};
...code removed for brevity...
};
Modernizr.load([{
test: window.matchMedia,
nope: 'matchMedia.js',
complete: function() {
var screenQuery = window.matchMedia('screen AND (max-width: 500px)');
deviceConfig.smallScreen = ko.observable(screenQuery.matches);
if (screenQuery.addListener) {
screenQuery.addListener(function(mq) {
deviceConfig.smallScreen(mq.matches);
});
}
deviceConfig.largeScreen = ko.computed(function() {
return !deviceConfig.smallScreen();
});
}
}, {
test: Modernizr.touch,
yep: 'jquery.touchSwipe-1.2.5.js',
callback: function() {
$('html').swipe({
swipeLeft: advanceCategory,
swipeRight: advanceCategory
})
}
},{
complete: function() {
deviceConfig.mobile = Modernizr.touch && deviceConfig.smallScreen();
callback(deviceConfig);
}
}]);
Ihaveaddedamobilepropertytotheviewmodel;itreturnstrueifthedevicemeetsmycriteriafor
gettingthemobileversionofmywebapp.Listing8-3showshowIhaveusedthisnewpropertyin
example.html.
Listing8-3.UsingMobileDeviceDetectionintheMainWebAppDocument
var cheeseModel = {};
detectDeviceFeatures(function(deviceConfig) {
cheeseModel.device = deviceConfig;
if (cheeseModel.device.mobile) {
location.href = "mobile.html";
}
197
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
$.getJSON("products.json", function(data) {
cheeseModel.products = data;
...
}).success(function() {
$(document).ready(function() {
IaddthecapabilitiescheckbeforetheJSONdataisloadedsothatIcandirecttheuserto
mobile.htmlbeforeIstartmakingnetworkrequestsandprocessingtheelementsintheDOM.
TipInthisexampleandthepreviousone,IplacedthemobiledetectioncodeoutsideofthejQueryreadyevent
sothatthebrowserwillexecutethecodeassoonasitreachesitinthedocument.Amorethoroughapproach
wouldbetoplacethedetectioncoderightatthetopofthedocumentsothatitisexecutedbeforeanyofthe
JavaScriptlibrariesareloaded.However,sinceIrelyonsomeoftheselibrariestoactuallyperformthedetection,
carefulorderingofthescriptelementsisrequired.
CreatingaSimpleMobileWebApp
BothoftheapproachesIshowedyouassumethattheuserwillwanttoviewthemobileversionofmy
webapp—butthiswon’talwaysbethecase.Iprefertoidentifyamobiledeviceandthenasktheuser
whattheywanttodo.Thisapproachputscontrolintousers’hands(whichiswhereitshouldbe),butit
doesmeanthatIhavetoprovideamechanismforlettingthemchooseandrememberingthechoice
theymake.So,ratherthansimplydirectingmobiledevicestothemobileversionofthewebapp,Iusean
interimdocumentcalledaskmobile.html.IplacedthisfileintheNode.jscontentdirectory,andyoucan
seethefilecontentinListing8-4.ThisisaverysimplewebappthatusesjQueryandjQueryMobile.
Listing8-4.AskingtheUserIfTheyWanttoUsetheMobileVersionoftheWebApp
<!DOCTYPE html>
<html>
<head>
<title>CheeseLux</title>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script src="jquery.mobile-1.0.1.js" type="text/javascript"></script>
<link rel="stylesheet" type="text/css" href="jquery.mobile-1.0.1.css"/>
<link rel="stylesheet" type="text/css" href="styles.mobile.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script>
function setCookie(name, value, days) {
var date = new Date();
date.setTime(date.getTime()+(days * 24 * 60 * 60 *1000));
document.cookie = name + "="+ value
+ "; expires=" + date.toGMTString() +"; path=/";
}
198
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
$(document).bind("pageinit", function() {
$('button').click(function(e) {
var useMobile = e.target.id == "yes";
var useMobileValue = useMobile ? "mobile" : "desktop";
if (localStorage) {
localStorage["cheeseLuxMode"] = useMobileValue;
} else {
setCookie("cheeseLuxMode", useMobileValue, 30);
}
location.href = useMobile ? "mobile.html" : "example.html";
});
});
</script>
</head>
<body>
<div id="page1" data-role="page" data-theme="a">
<img class="logo" src="cheeselux.png">
<span class="para">
Would you like to use our mobile web app?
</span>
<div class="middle">
<button data-inline="true" data-theme="b" id="yes">Yes</button>
<button data-inline="true" id="no">No</button>
</div>
</div>
</body>
</html>
TipIexplainhowtogettheCSSandJavaScriptfilesreferredtointhislistingshortly.
Thisdocumentpresentstheuserwithtwobuttonsthattheycanusetochoosetheversionofthe
webapptheywanttouse.YoucanseehowthedocumentisdisplayedinthebrowserinFigure8-1.
199
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
Figure8-1.Askingtheuserwhichversionofthewebapptheyrequire
ThistinywebappgivesmeagoodexamplewithwhichtointroducejQueryMobile,whichiswhat
I’llbeusinginthischapter.jQueryMobileisatoolkitoptimizedformobiledevices,anditincludes
widgetsthatareeasytointeractwithusingtouchandbuilt-insupportforhandlingtoucheventsand
gestures.
jQueryMobileisthe“official”mobiletoolkitfromthemainjQueryproject,andit’sprettygood,
althoughtherearesomeroughedgeswithsomelayoutsthatneedtweakingwithminorCSS.Thereare
otherjQuery-basedmobilewidgettoolkitsavailable—andsomeofthemareverygoodaswell.Ihave
chosenjQueryMobilebecauseitsharesabroadlycommonapproachwithjQueryUIandithassome
designcharacteristicsthataretypicalofmostmobiletoolkitsandthatrequirespecialattentionwhen
writingcomplexwebapps.
AVOIDING PSEUDONATIVE MOBILE APPS
AnotherreasonthatIusejQueryMobileisthatitdoesn’ttrytore-createtheappearanceofanative
smartphoneapplication,whichisanapproachthatsomeoftheothertoolkitsadopt.Idon’tlikethat
approachbecauseitdoesn’tquitework.IfyougivetheusersomethingthatlookslikeanativeiOSor
Androidapp,thenyouneedtomakesureitbehavesexactlythewayanativeapplicationshould—and,at
leastatthemoment,thatisn’tpossible.
Theworstpossibleapproachistotrytore-createanativeappforjustoneplatform.Youoftenseethis,
anditisusuallyiOSthatwebappdevelopersaimfor.Thismightnotbesobadifthere-creationwas
faithfulandallmobiledevicesraniOS,butusersofAndroidandotheroperatingsystemsgetsomething
thatistotallyalien,andiOSusersgetsomethingthatinitiallyappearstobefamiliarbutthatturnsouttobe
confusingandinconsistent.
Tomymind,itisfarbettertodesignawebappthatisgenuinelyobviousandeasytouse.Theresultsare
better,youuserswillbehappier,andyoudon’thavetocontortyourwebapptofitinsidetheconstraintsof
platformthatyoucan’tproperlyadheretoanyway.
200
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
IamnotgoingtoprovidealengthytutorialonjQueryMobile,buttherearesomeimportant
characteristicsthatIneedtoexplaininordertodemonstratehowtocreateasolidmobileweb
application.Iexplainthecoreconceptsinthesectionsthatfollow.Ifyouwantmoreinformationabout
jQueryMobile,thenseetheprojectwebsiteormyProjQuerybook,whichispublishedbyApressand
containsacompletereferenceforusingjQueryMobile.
InstallingjQueryMobile
YoucandownloadjQueryMobilefromhttp://jquerymobile.com.jQueryMobiledependsonjQuery,
andthescriptelementthatimportsjQueryintothedocumentmustcomebeforetheonethatimports
thejQueryMobilelibrary,likethis:
<head>
<title>CheeseLux</title>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script src="jquery.mobile-1.0.1.js" type="text/javascript"></script>
<link rel="stylesheet" type="text/css" href="jquery.mobile-1.0.1.css"/>
jQueryMobilereliesonitsownCSSandimagesthataredifferentfromthoseusedbyjQueryUI.
WhenyoudownloadjQueryMobile,copytheCSSfileintotheNode.jscontentdirectoryalongwiththe
JavaScriptfile,andputtheimagesintotheimagesdirectoryalongwiththosefromjQueryUI.
UnderstandingthejQueryMobileDataAttributes
jQueryMobilereliesondataattributestoconfigurethelayoutofthewebapp.Dataattributesallow
customattributestobeappliedtoelements,justlikethedata-bindattributethatIhavebeenusingfor
databindings.Thereisnodata-bindattributedefinedintheHTMLspecification,butanyattributethat
isprefixedbydata-isignoredbythebrowserandallowsyoutoembedusefulinformationinyour
markupthatyoucanthenaccessviaJavaScript.Dataattributeshavebeenusedunofficiallyforafew
yearsandareanofficialpartofHTML5.
jQueryMobileusesdataattributesratherthanthecode-centricapproachthatjQueryUIrequires.
Youusethedata-roleattributetotelljQueryMobilehowitshouldtreatanelement—themarkupis
processedautomaticallywhenthedocumentisloadedandthewidgetsarecreated.
Youdon’talwaysneedtousethedata-roleattribute.Forsomeelements,jQueryMobilewill
assumethatitneedstocreateawidgetbasedontheelementtype.Thishashappenedforthebuttonsin
thedocument:jQueryMobilewillcreateabuttonwidgetwhenitfindsabuttonelementinthemarkup.
So,thiselement:
<button data-inline="true" id="no">No</button>
doesn’tneedadata-roleattributebutcouldhavebeenwrittenlikethisifyouprefer:
<button data-role="button" data-inline="true" id="no">No</button>
DefiningPages
Themostimportantvalueforthedata-roleattributeispage.Whenbuildingmobilewebapps,itisgood
practicetominimizethenumberofrequestsmadetotheserver.jQueryMobilehelpsinthisregardby
supportingsingle-pageapps,wherethemarkupandscriptformultiplelogicalpagesiscontainedwithin
asingledocumentandshowntotheuserasrequired.Apageisdenotedbyadivelementwhosedataroleattributeispage.Thecontentofthedivelementisthecontentofthatpage:
201
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
...
<body>
<div id="page1" data-role="page" data-theme="a">
...page content goes here...
</div>
</body>
...
Thereisjustonepageinmyaskmobile.htmldocument,butI’llreturntothetopicofpageswhenwe
buildthefullmobileCheeseLuxapplaterinthechapter.
ConfiguringWidgets
jQueryMobilealsousesdataattributestoconfigurewidgets.Bydefault,jQueryMobilebuttonsspanthe
entirepage.Thisgivesalargetargettohitonasmallportraitscreenbutlooksprettyoddinother
layouts.Todisablethisbehavior,IhavetoldjQueryMobilethatIwantinlinebuttons,wherethebutton
isjustlargeenoughtocontainitscontent.Ididthisbysettingthedata-inlineattributetotrue forthe
buttonelements,likethis:
<button data-inline="true" id="no">No</button>
Anumberofelement-specificdataattributesareavailable,andyoushouldconsultthejQuery
Mobilewebsitefordetails.OneimportantconfigurationattributethatIwillmention,however,isdatatheme,whichappliesastyletothepageorwidgettowhichitisapplied.AjQueryMobilethemecontains
anumberofswatches,namedA,B,C,andsoon.Ihavesetthedata-themeattributetoaforthepage
elementsoastosetthethemeforthesinglepageinthedocumentandallofitscontent:
<div id="page1" data-role="page" data-theme="a">
YoucancreateyourowncustomthemesusingthejQueryMobileThemeRoller,whichisavailableat
jquerymobile.com.Iamusingthedefaultthemes,andswatchAprovidesthedarkstyleforthewebapp.
Forcontrast,IhavesettheswatchontheYesbuttontob,likethis:
<button data-inline="true" data-theme="b" id="yes">Yes</button>
ButtonsinswatchBareblue,whichgivestheuserastrongsuggestionastotherecommended
decision.
TipIhavedefinedanewCSSstylesheetforusewithjQueryMobile.Itiscalledhttp://styles.mobile.css,
anditlivesintheNode.jscontentdirectoryalongwiththeotherexamplefiles.Thestylesinthisfilejusttweakthe
layoutslightly,allowingmetocenterelementsinthepageandmakeotherminoradjustmentstothedefaultjQuery
Mobilelayout.Youcanfindthestylesheetinthesourcecodedownloadforthisbook,whichisavailablefrom
Apress.com.
202
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
DealingwithjQueryMobileEvents
UsingawidgetlibrarythatisbasedonjQuerymeanswecanhandleeventsusingfamiliartechniques.If
youlookatthescriptelementintheaskmobile.htmldocument,youwillseethathandlingtheevents
triggeredwhenthebuttonsareclickedrequiresthesamebasicjQuerycodethatIhavebeenusing
throughoutthisbook:
<script>
...code removed for brevity...
$(document).bind("pageinit", function() {
$('button').click(function(e) {
var useMobile = e.target.id == "yes";
var useMobileValue = useMobile ? "mobile" : "desktop";
if (localStorage) {
localStorage["cheeseLuxMode"] = useMobileValue;
} else {
setCookie("cheeseLuxMode", useMobileValue, 30);
}
location.href = useMobile ? "mobile.html" : "example.html";
});
});
</script>
IusejQuerytoselectthebuttonelementsandthestandardclickmethodtohandletheclickevent.
However,thereisoneveryimportantdifferenceinthewaythatjQueryMobiledealswithevents.Hereit
is:
$(document).bind("pageinit", function() {
...code to handle button click events...
}
jQueryMobileprocessesthemarkupfordataattributeswhenthestandardjQueryreadyeventfires.
ThismeansIhavetobindtothepageiniteventifIwanttoexecutecodeafterjQueryMobilehasfinished
settingupitswidgets.ThereisnoconvenientmethodforspecifyingafunctionforthiseventandsoI
haveusedthebindmethodinstead.ThecodeinthisexamplewouldhaveruninresponsetothejQuery
readyeventquitehappily,sinceIamnotinteractingdirectlywiththewidgetsthatjQueryMobilecreates.
ThiswillchangewhenIcometothefulljQueryMobileCheeseLuxwebapp,anditisgoodpracticetouse
thepageiniteventinalljQueryMobileapps.
StoringtheUser’sDecision
NowthatIhavedescribedthejQueryMobilepartsofaskmobile.html,wecanreturntotheapplication’s
function,whichistorecordandstoretheuser’spreferencefortheversionofthewebapptheuserwants
touse.Iuselocalstorageifitisavailableandfallbacktoaregularcookieifitisnot.Thereisno
convenientjQuerysupportforworkingwithcookies,soIhavewrittenmyownfunctioncalled
setCookie:
203
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
function setCookie(name, value, days) {
var date = new Date();
date.setTime(date.getTime()+(days * 24 * 60 * 60 *1000));
document.cookie = name + "="+ value
+ "; expires=" + date.toGMTString() +"; path=/";
}
IfIhavetousethecookie,thenIsetthelifetobe30days,afterwhichthebrowserwilldeletethe
cookieandtheuserwillhavetoexpresstheirpreferenceagain.Forbrevity,Ihavenotsetanylifetime
whenusinglocalstorage,butdoingsowouldbegoodpractice.
TipItisalsogoodpracticetoasktheuseriftheywantyoutostoretheirchoiceatall.Ihaven’ttakenthisstep
inmysimpleexample,butsomeusersaresensitivetotheseissues,especiallywhenitcomestocookies.
DetectingtheUser’sDecisionintheWebApp
Thelaststepistodetecttheuser’sdecisioninthedesktopversionoftheCheeseLuxwebapp.Listing8-5
showsapairoffunctionsIhaveaddedtoutils.jstosupportthisprocess.
Listing8-5.CheckingforaPriorDecisionBeforePerformingaRedirect
function checkForVersionPreference() {
var previousDecision;
if (localStorage && localStorage["cheeseLuxMode"]) {
previousDecision = localStorage["cheeseLuxMode"];
} else {
previousDecision = getCookie("cheeseLuxMode");
}
if (!previousDecision && cheeseModel.device.mobile) {
location.href = "/askmobile.html";
} else if (location.pathname == "/mobile.html" && previousDecision == "desktop") {
location.href = "/example.html";
} else if (location.pathname != "/mobile.html" && previousDecision == "mobile") {
location.href = "/mobile.html";
}
}
function getCookie(name) {
var val;
$.each(document.cookie.split(';'), function(index, elem) {
var cookie = $.trim(elem);
if (cookie.indexOf(name) == 0) {
val = cookie.slice(name.length + 1);
}
})
return val;
}
204
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
ThecheckForVersionPreferencefunctionusestheviewmodelvaluestoseewhethertheuserhasa
mobiledeviceand,ifso,triestorecovertheresultofapreviousdecisionfromlocalstorageoracookie.
Cookiesareawkwardtoprocess,soIhaveaddedagetCookiefunctionthatfindsacookiebynameand
returnsitsvalue.Ifthereisnostoredvalue,thenIdirecttheusertotheaskmobile.htmldocumenttoget
theirpreference.Ifthereisastoredvalue,thenIuseittoswitchtothemobileversionifthatwasthe
user’spreference.AllthatremainsistoincorporateacalltothecheckForVersionPreferencefunction
intoexample.html,whichcontainsthedesktopversionofthewebapp,likethis:
...
detectDeviceFeatures(function(deviceConfig) {
cheeseModel.device = deviceConfig;
checkForVersionPreference();
$.getJSON("products.json", function(data) {
cheeseModel.products = data;
}).success(function() {
$(document).ready(function() {
... code removed for brevity...
});
});
)};
...
IhaveshownthechangesascodesnippetsbecauseIdon’twanttousepagesinachapteronmobile
devicestolistthedesktopwebappcode.Youcangetthecompletelistingaspartofthesourcecode
downloadavailablefreeofchargefromApress.com.
TipItmakessensetooffertheuserthechancetochangetheirmindswhentheeffectofthedecisionisstored
andappliedautomatically.IskippedthisstepbecauseIwanttofocusonthemobileappinthischapter,butyou
shouldalwaysincludesomekindofUIcuethatallowstheusertoswitchtotheotherversionofthewebapp,
especiallyifthedecisionisstoredandusedpersistently.
BuildingtheMobileWebApp
IamgoingtostartwithabasicmobileversionoftheCheeseLuxwebappandthenbuildonittoshow
youhowtocreateabetterexperiencefortheuser.WhenIcreateamobileversionofawebappthathas
adesktopcounterpart,Ihavetwogoalsinmind:
•
Reuseasmuchdesktopcodeasispossible
•
Ensurethatthemobilerespondselegantlytodifferentdevicecapabilities
Thefirstgoalisallaboutlong-termmaintainability.ThemorecommoncodeIhave,thefewer
occasionstherewillbewhereIhavetofindandfixabugintwodifferentplaces.Iliketodecidein
advancewhichversionofthewebapphasprimacyandwhichwillhavetoflextobeabletousethecode.
205
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
Ingeneral,Itendtocreatethedesktopversionfirstandmakethemobilewebappadapt.Theexception
tothisiswhenthemajorityofuserswillbeusingmobiledevices.
WHAT ABOUT MOBILE FIRST?
Thereisaview(oftenreferredtoasmobilefirst)thatfocusesonthedesignanddevelopmentofthemobile
platformfirst,largelybecauseitforcesyoutoworkwithinthemostconstrainedenvironmentyouwillbe
targetingandbecausemobiledeviceshavecapabilities,likegeolocation,thatarenotondesktops.
Inmyprojects,Idon’twantinitialconstraints—Iwanttobuildtherichest,deepest,andmostimmersive
experienceIcan,and,forthemomentatleast,thatisthedesktop.OnceIhaveahandleonwhatis
possiblewithlargescreensandrichinteraction,Ibegintheprocessofdealingwithdeviceconstraints,
paringdownandtailoringmyappuntilIgetsomethingthatworkswellonamobiledevice.Iamnota
believerintheuniquecapabilitiesofmobiledevices,either.AsImentionedinChapter7,thehardandfast
distinctionsbetweencategoriesofdevicesarefadingfast.Oneofmymomentsofwonderrecentlywas
whenGooglewasabletousetheWi-FidataitcollectsalongwithitsStreetViewproducttopinpointmy
locationwithinafewfeet.Thiswasonamachinethatwouldrequireaforklifttrucktobemobile.
But,asImentionedpreviously,Iamnotapatternzealot,andyoushouldfollowwhateverapproachmakes
themostsenseforyouandyourprojects.Don’tletanyonedictateyourdevelopmentstyle,includingme.
Thesecondgoalisaboutensuringthatmymobilewebappisresponsiveandadaptstothewide
rangeofdevicetypesthatusersmayhave.Youcannotaffordtomakeassumptionsaboutscreensizeand
inputmechanismsevenwhentargetingjustmobiledevices.
CautionYoumaybetemptedtotrytocreateawebappthatswitchesbetweenjQueryUIandjQueryMobile(or
equivalentlibraries)basedonthekindofdevicethatisbeingused.Suchatrickispossiblebutincrediblyhardto
pulloffwithoutcreatingalotofverycontortedcodeandmarkup.Themostsensibleapproachistocreateseparate
versionsifyouwanttotakeadvantageoffeaturesthatarespecifictoonelibraryoranother.
Togetthingsgoing,Listing8-6showsafirstpassatcreatingthecorefunctionalityusingjQuery
Mobile.ThislistingdependsonsomechangesintheviewmodelthatI’llexplainshortly.
Listing8-6.TheInitialVersionoftheCheeseLuxMobileWebApp
<!DOCTYPE html>
<html>
<head>
<title>CheeseLux</title>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).bind("mobileinit", function() {
$.mobile.autoInitializePage = false;
206
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
});
</script>
<script src="jquery.mobile-1.0.1.js" type="text/javascript"></script>
<link rel="stylesheet" type="text/css" href="jquery.mobile-1.0.1.css"/>
<link rel="stylesheet" type="text/css" href="styles.mobile.css"/>
<script src='knockout-2.0.0.js' type='text/javascript'></script>
<script src='utils.js' type='text/javascript'></script>
<script src='signals.js' type='text/javascript'></script>
<script src='crossroads.js' type='text/javascript'></script>
<script src='hasher.js' type='text/javascript'></script>
<script src='modernizr-2.0.6.js' type='text/javascript'></script>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script>
var cheeseModel = {};
detectDeviceFeatures(function(deviceConfig) {
cheeseModel.device = deviceConfig;
checkForVersionPreference();
$.getJSON("products.json", function(data) {
cheeseModel.products = data;
enhanceViewModel();
$(document).ready(function() {
ko.applyBindings(cheeseModel);
$('button#left, button#right').live("click", function(e) {
e.preventDefault();
advanceCategory(e, e.target.id);
})
$.mobile.initializePage();
});
});
$(document).bind("pageinit", function() {
function positionCategoryButtons() {
setTimeout(function() {
$('fieldset:visible').each(function(index, elem) {
var fsWidth = 0;
$(elem).children().each(function(index, child) {
fsWidth+= $(child).width();
});
if (fsWidth > 0) {
$(elem).width(fsWidth);
} else {
positionCategoryButtons();
}
});
}, 10);
};
positionCategoryButtons();
cheeseModel.device.smallAndPortrait.subscribe(positionCategoryButtons);
});
207
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
});
</script>
</head>
<body>
<div id="page1" data-role="page" data-theme="a">
<div id="logobar" data-bind="visible: device.largeScreen()">
<img data-bind="ifAttr: {attr: 'src', value: 'cheeselux.png',
test: device.largeScreen()}">
<span id="tagline">Gourmet European Cheese</span>
</div>
<fieldset class="middle" data-role="controlgroup" data-type="horizontal"
data-bind="foreach:products, visible: device.largeScreen() ||
device.smallAndPortrait()">
<input type="radio" name="category" data-bind="attr: {id: category,
value: category}, checked: $root.selectedCategory" />
<label data-bind="attr: {for: category}">
<span data-bind="text: $root.device.smallAndPortrait()?
shortName : category"></span>
</label>
</fieldset>
<form action="/basket" method="post">
<div data-bind="foreach: products">
<div data-bind="fadeVisible: category == $root.selectedCategory()">
<div data-role="header" >
<h1 data-bind="text: category"></h1>
</div>
<!-- ko foreach: items -->
<div class="itemContainer ui-grid-a">
<div class="ui-block-a">
<label data-bind="attr: {for: id}, formatText: {value: name,
suffix:':'}"></label>
</div>
<div class="ui-block-b">
<input data-bind="attr: {name: id}, value: quantity">
</div>
</div>
<!-- /ko -->
<div data-role="footer">
<h1>
<label>Total:</label>
<span data-bind="formatText: {prefix: '$',
value: cheeseModel.total()}"
</h1>
</div>
</div>
</div>
<div class="middle" data-role="controlgroup" data-type="horizontal"
data-bind="visible: device.smallAndLandscape()">
<button id="left" data-icon="arrow-l"> </button>
208
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
<input type="submit" value="Submit Order"/>
<button id="right" data-icon="arrow-r"
data-iconpos="right"> </button>
</div>
<div class="middle" data-role="controlgroup" data-type="horizontal"
data-bind="visible: !device.smallAndLandscape()">
<input type="submit" value="Submit Order"/>
</div>
</form>
</div>
</body>
</html>
Forthemostpart,thisisastraightforwardwebappthatreliesonthecorefunctionalityofjQuery
Mobile,butyouneedtobeawareofsomewrinklesandadditionsthatIdescribeinthefollowing
sections.Youcanseethelandscapeandportraitlayoutsforasmall-screendeviceinFigure8-2.Theweb
appalsosupportslayoutsformobiledeviceswithlargerscreens.Ihavenotshowntheselayouts,but
theyaresimilartothoseshowninthefigure,butwiththeCheeseLuxlogoandthefullcategorynames
displayedinthenavigationbuttons.
Figure8-2.ThebasicimplementationofthemobileCheeseLuxwebapp
Youwillnoticenewdatabindingsandviewmodelitemsinthislisting.TheformatTextdatabinding
letsmeapplyaprefixandsuffixtothetextcontentofanelement,whichsimplifiesworkingwith
209
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
composedstrings,especiallycurrencyamounts.ThisisoneofthesetofcustombindingsthatIgenerally
addtoprojectsandthecode,whichisincludedintheutils.jsfile,asshowninListing8-7.The
composeStringfunctionusedbythisbindingisthesameoneIshowedyouinChapter4whenI
introducedthecustomformatAttrbinding.
Listing8-7.TheformatTextCustomDataBinding
ko.bindingHandlers.formatText = {
update: function(element, accessor) {
$(element).text(composeString(accessor()));
}
}
Theotheradditionsaresomehelpfulshortcutsaddedtothedevicecapabilitiesinformationinthe
viewmodel.AlthoughKOcandealwithexpressionsindatabindings,Idon’tlikedefiningcodeinthis
way,andIgenerallycreatecomputeddataitemsthatallowmetodeterminethestateofthedevice
throughasingleviewmodelitem.Forthischapter,Idefinedapairofcomputedvaluesthatletmeeasily
readthecombinationsofscreensizeandorientationthatIaminterestedinforthemobilewebapp.
TheseshortcutsaredefinedinthedetectDeviceFeaturesfunctionintheutils.jsfile,asshownin
Listing8-8.
Listing8-8.CreatingShortcutsintheViewModeltoAvoidExpressionsinBindings
...
function detectDeviceFeatures(callback) {
var deviceConfig = {};
deviceConfig.landscape = ko.observable();
deviceConfig.portrait = ko.computed(function() {
return !deviceConfig.landscape();
});
var setOrientation = function() {
deviceConfig.landscape(window.innerWidth > window.innerHeight);
}
setOrientation();
$(window).bind("orientationchange resize", function() {
setOrientation();
});
setInterval(setOrientation, 500);
if (window.matchMedia) {
var orientQuery = window.matchMedia('screen AND (orientation:landscape)')
if (orientQuery.addListener) {
orientQuery.addListener(setOrientation);
}
}
Modernizr.load([{
test: window.matchMedia,
210
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
nope: 'matchMedia.js',
complete: function() {
var screenQuery = window.matchMedia('screen AND (max-width: 500px)');
deviceConfig.smallScreen = ko.observable(screenQuery.matches);
if (screenQuery.addListener) {
screenQuery.addListener(function(mq) {
deviceConfig.smallScreen(mq.matches);
});
}
deviceConfig.largeScreen = ko.computed(function() {
return !deviceConfig.smallScreen();
});
setInterval(function() {
deviceConfig.smallScreen(window.innerWidth <= 500);
}, 500);
}
}, {
},{
test: Modernizr.touch,
yep: 'jquery.touchSwipe-1.2.5.js',
callback: function() {
$('html').swipe({
swipeLeft: advanceCategory,
swipeRight: advanceCategory
})
}
complete: function() {
deviceConfig.mobile = Modernizr.touch && deviceConfig.smallScreen();
deviceConfig.smallAndLandscape = ko.computed(function() {
return deviceConfig.smallScreen() && deviceConfig.landscape();
});
deviceConfig.smallAndPortrait = ko.computed(function() {
return deviceConfig.smallScreen() && deviceConfig.portrait();
});
callback(deviceConfig);
}
};
...
}]);
ManagingtheEventSequence
AsIdemonstratedintheaskmobile.htmldocument,jQueryMobilewillprocessadocument
automaticallyandcreatewidgetsbasedonelementtypesandthevalueofthedata-roleattribute.Thisis
anicefeature,anditsignificantlyreducestheamountofcoderequiredforsimplewebapps.
Unfortunately,itgetsinthewaywhenyouareusingtheviewmodeltogenerateorformatelements,
especiallyifthedataintheviewmodelisobtainedviaAjax.jQueryMobilewillprocessthedocument
211
www.it-ebooks.info
w
CHAPTER8CREATINGMOBILEWEBAPPS
beforetheviewmodelispopulatedwiththedatabindings,whichmeansthatwidgetsarenotcreated
properly.
ThisisthesameproblemIencounteredpreviouslywithjQueryUI,buttheissueisworsewith
jQueryMobilebecauseitassumesthatithassolecontrolofelementsinapageandmakesitvery
difficulttocreatebindingsthatcannegotiatetheextraelementsthatjQueryMobileuseswhenitsetsup
awidget.(ThisisaproblemI’llreturntofordifferentreasonslaterinthischapter.)
DisablingAutomaticProcessing
ThebestapproachistopreventjQueryMobilefromautomaticallyprocessingthedocument.Todothis,
Ineedtohandlethemobileinitevent,whichisemittedbyjQueryMobilewhenthelibraryisfirstloaded.
IneedtoregistermyhandlerfunctionbeforejQueryMobileisloaded,whichmeansIhavetoinserta
newscriptelementaftertheonethatimportsjQueryandbeforetheonethatimportsjQueryMobile,
likethis:
...
<sript src="jquery-1.7.1.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).bind("mobileinit", function() {
$.mobile.autoInitializePage = false;
});
</script>
<script src="jquery.mobile-1.0.1.js" type="text/javascript"></script>
...
Bysettingthe$.mobile.autoInitializePagepropertytofalse,IdisablethejQueryMobilefeature
thatprocessesthemarkupinthedocumentautomatically.
TipTobefair,IneedtoinsertmyscriptelementafterjQueryonlyifIwanttousethebindmethod,butI
prefertodothisratherthanusetheclunkyDOMAPIforhandlingevents.
DisablingtheautomaticprocessingstopstheracebetweentheviewmodelandjQueryMobileand
allowsmetomakemyAjaxrequest,populatetheviewmodel,anddoanyothertasksIneedwithout
worryingaboutprematurewidgetcreation.WhenIamdonesettingup,IexplicitlytelljQueryMobile
thatitshouldprocessthepage,likethis:
$.getJSON("products.json", function(data) {
cheeseModel.products = data;
enhanceViewModel();
$(document).ready(function() {
ko.applyBindings(cheeseModel);
$('button#left, button#right').live("click", function(e) {
e.preventDefault();
advanceCategory(e, e.target.id);
})
$.mobile.initializePage();
212
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
});
});
ThemobileobjectprovidesaccesstothejQueryMobileAPI,andtheinitializePagemethodstarts
pageprocessing.
RespondingtothepageinitEvent
NowthatIhavethemaineventsundercontrol,IcanusethepageinittoperformtasksafterjQuery
Mobilehasprocessedthepagesinthedocument.jQueryMobileisgenerallyverysolid,butithassome
layoutquirks.Oneinparticularisthatgroupsofbuttonsarenotcenteredinthepage.Forthebuttonsat
thebottomofthepage,IhavebeenabletofixthisissuewithCSS(whichiswhatthecenteredstyleisfor
inthestyles.mobile.cssfile).Butthesizeofthenavigationbuttonschanges,andthatrequiresa
JavaScriptsolution,whichisasfollows:
...
$(document).bind("pageinit", function() {
function positionCategoryButtons() {
setTimeout(function() {
$('fieldset:visible').each(function(index, elem) {
var fsWidth = 0;
$(elem).children().each(function(index, child) {
fsWidth+= $(child).width();
});
if (fsWidth > 0) {
$(elem).width(fsWidth);
} else {
positionCategoryButtons();
}
});
}, 10);
};
positionCategoryButtons();
cheeseModel.device.smallAndPortrait.subscribe(positionCategoryButtons);
});
...
IwanttocenterthebuttonsafterjQueryMobilehasfinishedcreatingthem,whichisanidealusefor
thepageinitevent.Inthefunction,Iaddupthewidthofthechildrenofeachfieldsetelementandthen
usethetotalvaluetosetthewidthofthefieldset.jQueryMobileleavesthefieldsettobethewidthof
thewindow,andthesequenceofelementsrequiredtocreateasetofbuttonsmakesithardtocenterthe
buttonsbyothermeans.
TipIusethejQueryeachmethodsothatIcanbesurethatthechildrenmethodreturnsonlythechildrenof
onefieldsetelement.Thismeansmycodewon’tbreakifIaddanotherfieldsetelementlater.Element
selectorsaregreedy,andifIjustcall$('fieldset').children(),Iwillgetthechildrenofallfieldsetelements
inthedocument,whichwillthrowoutthewidthcalculations.
213
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
IwrappedthecodethatsetsthewidthinsideacalltothesetTimeoutfunctionbecauseIwantto
correctlyresizethefieldsetelementwhenthecontentofthenavigationbuttonschange,which
happenswhenthesizeandorientationarealtered.
Thecontentoftheelementsischangedbydatabindings,whichareexecutedwhenobservabledata
itemsintheviewmodelareupdated.SinceIamusingthesubscribemethodtoreceivethesamekindof
notifications,Ineedtomakesurethatmycodetoresizethefieldsetisn’texecutedbeforethebutton
contentischanged,whichIachievebyintroducingasmalldelayusingthesetTimeoutfunction.
PreparingforContentChanges
jQueryMobileassumesthatithascontroloftheelementsthatareusedasthefoundationforwidgets.In
thecaseofbuttons,jQueryMobilewrapsthebuttoncontents(orlabelcontentswhenusingradio
buttons)inaspanelementsothatstylingcanbeapplied.
ThisisthesameproblemthatjQueryUIcreates,andthesolutionisthesameforjQueryMobile:
wrapthecontentinaspanelementyourselfsothatyouhaveatargetfordatabindings.Onceyouhave
anelementthatyoucanattachdatabindingsto,youdon’tneedtoworryabouthowjQueryMobile
transformstheelementintoawidget.YoucanseehowIhavedonethisforthenavigationbuttons:
<fieldset class="middle" data-role="controlgroup" data-type="horizontal"
data-bind="foreach:products, visible: device.largeScreen() ||
device.smallAndPortrait()">
<input type="radio" name="category" data-bind="attr: {id: category,
value: category}, checked: $root.selectedCategory" />
<label data-bind="attr: {for: category}">
<span data-bind="text: $root.device.smallAndPortrait()?
shortName : category"></span>
</label>
</fieldset>
Thismayseemlikeasimpletrick,butalotofmobilewebappprogrammersgetcaughtbythisissue
andenduptryingtoresolveitthroughsometorturedandunreliablealternative.Thissimpleapproach
resolvestheproblemratherneatly.AllofthemobilewidgettoolkitsthatIhaveusedclashwithdata
bindingsinasimilarway.InthecaseofjQueryMobile,youknowthattheproblemhasoccurredwhen
theformattingofbuttonsislostwhenadatabindingchangesthebuttoncontent,asshownin
Figure8-3.
Figure8-3.ProblemscausedbyjQueryMobileaddingelementsforstyling
214
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
DuplicatingElementsandUsingTemplates
Notallconflictsbetweenwidgetlibrariesanddatabindingscanberesolvedsoeasily.InListing8-6,I
createdduplicatesetsofthebuttonsthataredisplayedatthebottomofthepage,likethis:
<div class="middle" data-role="controlgroup" data-type="horizontal"
data-bind="visible: device.smallAndLandscape()">
<button id="left" data-icon="arrow-l"> </button>
<input type="submit" value="Submit Order"/>
<button id="right" data-icon="arrow-r"
data-iconpos="right"> </button>
</div>
<div class="middle" data-role="controlgroup" data-type="horizontal"
data-bind="visible: !device.smallAndLandscape()">
<input type="submit" value="Submit Order"/>
</div>
Onesethasadditionalbuttonsthattheusercanclicktonavigatethroughtheproductcategories.
TheproblemthatIamworkingaroundisthatjQueryMobilecreatesasetofbuttonswithouttakinginto
accountthevisibilityoftheelementsitisworkingwith.Thatmeanstheouterbuttonsaregivenrounded
cornerseveniftheyareinvisible,whichmeansthatusingthevisiblebindingdoesn’tcreatewellformattedgroupsofbuttons.
TheifbindinghasitsownissuesbecausejQueryMobilewon’tautomaticallyupdatethestylingof
buttonswhennewelementsareaddedtothecontainer,andaskingjQueryMobiletorefreshthecontent
doesn’taddressthisissue.So,thesimplestapproachistocreateduplicatesetsofelements.
UsingTwo-PassDataBindings
DuplicatingelementsisOKforsimplesituations,butitbecomesproblematicwhenyouareworkingwith
complexsetsofelementsthathavealotofbindingsandformatting.Atsomepoint,achangewillbe
appliedtoonesetofelementsandnottheother.Trackingdownthiskindofissuewhenithappenscan
betime-consuming.Analternativeapproachistogenerateduplicatesetsofelementsfromasingle
template.Thisisanelegant,butfiddly,technique—youcanseethechangesrequiredinListing8-9.
Listing8-9.UsingaTemplatetoCreateDuplicateSetsofElements
<!DOCTYPE html>
<html>
<head>
<title>CheeseLux</title>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).bind("mobileinit", function() {
$.mobile.autoInitializePage = false;
});
</script>
<script src="jquery.mobile-1.0.1.js" type="text/javascript"></script>
<link rel="stylesheet" type="text/css" href="jquery.mobile-1.0.1.css"/>
<link rel="stylesheet" type="text/css" href="styles.mobile.css"/>
<script src='knockout-2.0.0.js' type='text/javascript'></script>
215
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
<script src='utils.js' type='text/javascript'></script>
<script src='signals.js' type='text/javascript'></script>
<script src='crossroads.js' type='text/javascript'></script>
<script src='hasher.js' type='text/javascript'></script>
<script src='modernizr-2.0.6.js' type='text/javascript'></script>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script>
var cheeseModel = {};
detectDeviceFeatures(function(deviceConfig) {
cheeseModel.device = deviceConfig;
checkForVersionPreference();
$.getJSON("products.json", function(data) {
cheeseModel.products = data;
enhanceViewModel();
});
$(document).ready(function() {
ko.applyBindings(cheeseModel);
$('*.deferred').each(function(index, elem) {
ko.applyBindings(cheeseModel, elem);
});
$('button#left, button#right').live("click", function(e) {
e.preventDefault();
advanceCategory(e, e.target.id);
})
$.mobile.initializePage();
});
$(document).bind("pageinit", function() {
function positionCategoryButtons() {
setTimeout(function() {
$('fieldset:visible').each(function(index, elem) {
var fsWidth = 0;
$(elem).children().each(function(index, child) {
fsWidth+= $(child).width();
});
if (fsWidth > 0) {
$(elem).width(fsWidth);
} else {
positionCategoryButtons();
}
});
}, 10);
};
positionCategoryButtons();
cheeseModel.device.smallAndPortrait.subscribe(positionCategoryButtons);
});
});
</script>
<script id="buttonsTemplate" type="text/html">
216
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
<div class="deferred middle" data-role="controlgroup" data-type="horizontal"
data-bind="attr: {'data-bind': 'visible: ' + ($data ? '' : '!')
+ 'device.smallAndLandscape()' }">
<!-- ko if: $data -->
<button id="left" data-icon="arrow-l"> </button>
<!-- /ko -->
<input type="submit" value="Submit Order"/>
<!-- ko if: $data -->
<button id="right" data-icon="arrow-r" data-iconpos="right"> </button>
<!-- /ko -->
</div>
</script>
</head>
<body>
<div id="page1" data-role="page" data-theme="a">
<div id="logobar" data-bind="visible: device.largeScreen()">
<img data-bind="ifAttr: {attr: 'src', value: 'cheeselux.png',
test: device.largeScreen()}">
<span id="tagline">Gourmet European Cheese</span>
</div>
<fieldset class="middle" data-role="controlgroup" data-type="horizontal"
data-bind="foreach:products, visible: device.largeScreen() ||
device.smallAndPortrait()">
<input type="radio" name="category" data-bind="attr: {id: category,
value: category}, checked: $root.selectedCategory" />
<label data-bind="attr: {for: category}">
<span data-bind="text: $root.device.smallAndPortrait()?
shortName : category"></span>
</label>
</fieldset>
<form action="/basket" method="post">
<div data-bind="foreach: products">
<div data-bind="fadeVisible: category == $root.selectedCategory()">
<div data-role="header" >
<h1 data-bind="text: category"></h1>
</div>
<!-- ko foreach: items -->
<div class="itemContainer ui-grid-a">
<div class="ui-block-a">
<label data-bind="attr: {for: id}, formatText: {value: name,
suffix:':'}"></label>
</div>
<div class="ui-block-b">
<input data-bind="attr: {name: id}, value: quantity">
</div>
</div>
<!-- /ko -->
<div data-role="footer">
<h1>
<label>Total:</label>
217
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
<span data-bind="formatText: {prefix: '$',
value: cheeseModel.total()}"
</h1>
</div>
</div>
</div>
<!-- ko template: {name: 'buttonsTemplate', foreach: [true, false] } -->
<!-- /ko -->
</form>
</div>
</body>
</html>
Thistechniquehasthreeparts,andtoshowhowthepartsfittogether,Ineedtoexplainthemin
reverseorderfromhowtheyappearinthedocument.
InvokingaTemplatewithCustomData
IhaveusedthetemplatebindingtogenerateelementsfromaKnockout.jstemplate,atechniquethatI
describedinChapter3:
<!-- ko template: {name: 'buttonsTemplate', foreach: [true, false] } -->
<!-- /ko -->
ThetwististhatIamnotusingtheviewmodeltodrivethetemplate.Instead,Ihavecreatedanarray
thatcontainstrueorfalsevalues.Iamapplyingthistechniqueinaverysimplesituation,andIneedto
knowonlyifIamcreatingthesetofbuttonsthatallowforcategorynavigation(representedbythetrue
value)orthesetthatdoesn’t(representedbythefalsevalue).Thepointisthatyoucanusetheforeach
bindingwithdatathatisnotpartoftheviewmodel.Youcanusemorecomplexdatastructuresformore
complexsetsofelements.
UsingaTemplatetoGenerateBindings
Thesecondstepisalittleodd.Iusetheattrdatabindingstosetthevalueofthedata-bindattributeon
theelementsthataregeneratedbythetemplate,likethis:
<script id="buttonsTemplate" type="text/html">
<div class="deferred middle" data-role="controlgroup" data-type="horizontal"
data-bind="attr: {'data-bind': 'visible: ' + ($data ? '' : '!')
+ 'device.smallAndLandscape()' }">
<!-- ko if: $data -->
<button id="left" data-icon="arrow-l"> </button>
<!-- /ko -->
<input type="submit" value="Submit Order"/>
<!-- ko if: $data -->
<button id="right" data-icon="arrow-r" data-iconpos="right"> </button>
<!-- /ko -->
</div>
</script>
218
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
Thesimplestpartofthetemplateistheuseoftheifbindingtofigureoutwhenthecategory
navigationbuttonsshouldbegenerated.Mytemplatewillbeusedtwice:onceeachforthetrueand
falsevaluesthatIpassedtotheforeachbinding.Whenthevalueistrue,thebuttonelementsare
includedintheDOM,andtheyareomittedwhenthevalueisfalse.
ThemorecomplexpartiswhereIhaveusedtheattrbindingtospecifyavaluethatIwantforthe
data-bindattributeintheelementsthataregeneratedbythetemplate.Hereisthevalueofthedata-bind
attributeinthetemplate:
data-bind="attr: {'data-bind': 'visible: ' + ($data ? '' : '!') +
'device.smallAndLandscape()'}"
Thereisalotgoingoninthisbinding.ThemostimportantthingtounderstandisthatIam
specifyingthedata-bindvalueIwantthegeneratedelementstohaveasastring,andthisstringwon’tbe
processedatthemoment.I’llreturntotheprocessingshortly.
Iuse$datatorefertothevaluesIpassedtotheforeachbindingwhenIcalledthetemplate.The
valueof$datawillbeeithertrueorfalse.First,Knockoutwillresolvethispartofthebinding,sowhenI
amdealingwiththetruevalue,thegenerateddivelementwillhaveabindinglikethis:
data-bind="attr: {'data-bind': 'visible: device.smallAndLandscape()'}"
andthefalsevaluewillcauseabindinglikethis:
data-bind="attr: {'data-bind': 'visible: !device.smallAndLandscape()'}"
Then,oncethedatavalueshavebeenresolved,Knockoutwillprocesstheentireattrbinding,which
hastheratherneateffectofreplacingitselfinthegeneratedelement,likethis:
data-bind="visible: device.smallAndLandscape()"
ReapplyingtheDataBindings
Knockoutprocessesthedata-bindattributeonlyonce,whichmeansthatmytemplategenerates
elementswiththedatabindingsthatIwant,butthesebindingsarenotlive.Changesintheviewmodel
won’taffectthembecausethedata-bindattributeswerenotdefinedwhenIcalledtheko.applyBindings
method.
Tofixthis,IsimplycallapplyBindingsagain,butthistimeIusetheoptionalargumentthatallows
metospecifywhichelementsareprocessed:
$(document).ready(function() {
ko.applyBindings(cheeseModel);
$('*.deferred').each(function(index, elem) {
ko.applyBindings(cheeseModel, elem);
});
$('button#left, button#right').live("click", function(e) {
e.preventDefault();
advanceCategory(e, e.target.id);
})
$.mobile.initializePage();
});
Iaddedmybuttoncontainerelementtothedeferredclass.Inowselectallmembersofthisclass
andusetheeachmethodtocalltheapplyBindingsmethodoneachelementinturn.Thismakes
219
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
Knockout.jsprocessthebindingsthatIgeneratedfromthetemplateandmakethemlive.Thisfinalstep
meansthatmybindingswillrespondtochangesintheviewmodel.
Thereareacoupleofpointstonoteaboutthistechnique.First,Iamnottryingtoprevent
duplicationofelementsintheDOM.ThereisnoeasywaytodealwiththejQueryMobileformatting
issueswithoutduplicateelementsets.Mygoalistogeneratetheduplicatesfromasinglesetofsource
elementssothatImakechangesinoneplaceandhavethemtakeeffectinalloftheduplicateswhen
theyaregenerated.
Second,whenusingthistechnique,youmustensurethatyoudon’trefertoviewmodelitemsexcept
withinapairofquotecharacters(i.e.,withinastring).Ifyourefertoavariableoutsideofastring,then
Kockout.jswilltrytofindavaluetoresolvethereference,andyouwillgetanerror.Viewmodelvalues
areresolvedinthesecondcalltotheapplyBindingsmethodandnotwhenthetemplateisusedtocreate
elements.
CautionItcanbedifficulttogetthestringproperlysetup,buttheeffortisworthwhileforcomplexsetsof
elements.Forsimplersituations,Isuggestyousimplyduplicatewhatyouneedinsidethedocumentandskipthe
templatesaltogether.Thesourcecodedownloadforthisbookcontainsthefulllistingsforthisexample.
AdoptingtheMultipageModel
Mymobilewebappisshapingup,butIamstillmissingURLrouting,whichmeansthereisasignificant
differencebetweenthemobileanddesktopversions.Thefirststepinaddingsupportforroutingisto
embracethemultipagemodel.AsIexplainedearlier,jQueryMobilesupportstheideaofhavingmultiple
pagesinasingleHTMLdocument.Iwillusethisfeaturetoprovidetheuserwiththemeanstonavigate
betweencategories.Listing8-10showsthechangesthatarerequired.
Listing8-10.AddingSupportfortheMultipageModel
<!DOCTYPE html>
<html>
<head>
<title>CheeseLux</title>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).bind("mobileinit", function() {
$.mobile.autoInitializePage = false;
});
</script>
<script src="jquery.mobile-1.0.1.js" type="text/javascript"></script>
<link rel="stylesheet" type="text/css" href="jquery.mobile-1.0.1.css"/>
<link rel="stylesheet" type="text/css" href="styles.mobile.css"/>
<script src='knockout-2.0.0.js' type='text/javascript'></script>
<script src='utils.js' type='text/javascript'></script>
<script src='signals.js' type='text/javascript'></script>
<script src='crossroads.js' type='text/javascript'></script>
<script src='hasher.js' type='text/javascript'></script>
<script src='modernizr-2.0.6.js' type='text/javascript'></script>
220
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
<meta name="viewport" content="width=device-width, initial-scale=1">
<script>
var cheeseModel = {};
detectDeviceFeatures(function(deviceConfig) {
cheeseModel.device = deviceConfig;
checkForVersionPreference();
$.getJSON("products.json", function(data) {
cheeseModel.products = data;
enhanceViewModel();
$(document).ready(function() {
ko.applyBindings(cheeseModel);
$('*.deferred').each(function(index, elem) {
ko.applyBindings(cheeseModel, elem);
});
$('button.left, button.right').live("click", function(e) {
e.preventDefault();
advanceCategory(e, $(e.target).hasClass("left")
? "left" : "right");
$.mobile.changePage($('div[data-category="'
+ cheeseModel.selectedCategory() + '"]'));
})
$.mobile.initializePage();
hasher.initialized.add(crossroads.parse, crossroads);
hasher.changed.add(crossroads.parse, crossroads);
hasher.init();
crossroads.addRoute("category/:newCat:", function(newCat) {
cheeseModel.selectedCategory(newCat ||
cheeseModel.products[0].category);
});
crossroads.addRoute("{shortCat}", function(shortCat) {
$.each(cheeseModel.products, function(index, item) {
if (item.shortName == shortCat) {
crossroads.parse("category/" + item.category);
}
});
});
crossroads.parse(location.hash.slice(1));
});
});
});
</script>
<script id="buttonsTemplate" type="text/html">
<div class="deferred middle" data-role="controlgroup" data-type="horizontal"
data-bind="attr: {'data-bind': 'visible: ' + ($data ? '' : '!')
221
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
+ 'device.smallAndLandscape()'}">
<!-- ko if: $data -->
<button class="left" data-icon="arrow-l"> </button>
<!-- /ko -->
<input type="submit" value="Submit Order"/>
<!-- ko if: $data -->
<button class="right" data-icon="arrow-r"
data-iconpos="right"> </button>
<!-- /ko -->
</div>
</script>
</head>
<body>
<!-- ko foreach: products -->
<div data-role="page" data-theme="a"
data-bind="attr: {'id': shortName, 'data-category': category}">
<div id="logobar" data-bind="visible: $root.device.largeScreen()">
<img data-bind="ifAttr: {attr: 'src', value: 'cheeselux.png',
test: $root.device.largeScreen()}">
<span id="tagline">Gourmet European Cheese</span>
</div>
<fieldset class="middle" data-role="controlgroup" data-type="horizontal"
data-bind="foreach: $root.products,
visible: $root.device.largeScreen() ||
$root.device.smallAndPortrait()">
<a data-role="button" data-bind="formatAttr: {attr: 'href',
prefix: '#', value: shortName},
css: {'ui-btn-active': (category == $root.selectedCategory())}">
<span data-bind="text: $root.device.smallAndPortrait()? shortName :
category"></span>
</a>
</fieldset>
<form action="/basket" method="post">
<div>
<div>
<div data-role="header" >
<h1 data-bind="text: category"></h1>
</div>
<!-- ko foreach: items -->
<div class="itemContainer ui-grid-a">
<div class="ui-block-a">
<label data-bind="attr: {for: id},
formatText: {value: name, suffix:':'}">
</label>
</div>
<div class="ui-block-b">
<input data-bind="attr: {name: id}, value: quantity">
</div>
</div>
222
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
<!-- /ko -->
<div data-role="footer">
<h1>
<label>Total:</label>
<span data-bind="formatText: {prefix: '$',
value: cheeseModel.total()}"
</h1>
</div>
</div>
</div>
<!-- ko template: {name: 'buttonsTemplate', foreach: [true, false] } -->
<!-- /ko -->
</form>
</div>
<!-- /ko -->
</body>
</html>
Ihavehighlightedthemostimportantchanges(andI’lldescribetheminamoment),butthebasic
approachistocreateonepagepercategory.Eachpagecontainsaduplicatesetofnavigationitems,and
onlythedetailsofindividualproductsdiffer.Forthemostpart,thechangesaretothedatabindingsto
createthiseffect.Somechanges,however,requiremoreexplanation.
ReworkingCategoryNavigation
jQueryMobileusesthesameURL-fragment-basedapproachIemployedinthedesktopversionto
navigatebetweenpages.Forexample,ifthereisadivelementwhosedata-roleattributeissettopage
andwhoseidattributeissettomypage,IcangetjQueryMobiletodisplaythatpagebynavigatingtothe
#mypagefragment.
ThedifferencefromthedesktopwebappisthatjQueryMobileplacessomeconstraintsonthe
namesthatcanbeusedforpages.Iusedthefullcategorynamebefore(suchasBritish Cheese),but
spacesareaproblemforjQueryMobile,soIhaveusedtheshortcategorynameinstead(British,for
example).HereisthebindingthatsetsthepageID:
<div data-role="page" data-theme="a"
data-bind="attr: {'id': shortName, 'data-category': category}">
NoticethatIhaveaddedadata-categoryattributethatcontainsthefullcategoryname.I’llreturnto
thisattributeshortly.
ReplacingRadioButtonswithAnchors
ThepagenavigationmodelmeansthatIcanreplacemyradiobuttonswithaelements.jQueryMobile
willcreatebuttonwidgetsfromanaelementifthedata-roleattributeissettobutton,andthevalueof
thehrefattributecanbeusedfornavigationwithinthedocument:
<a data-role="button" data-bind="formatAttr: {attr: 'href',
prefix: '#', value: shortName},
css: {'ui-btn-active': (category == $root.selectedCategory())}">
<span data-bind="text: $root.device.smallAndPortrait()? shortName :
category"></span>
</a>
223
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
Whenthedatabindingsareresolved,Igetanavigationelementwhosepurposeisaloteasierto
divine:
<a data-role="button" href="#British"
<span>British</span>
</a>
ClickingoneofthebuttonsthatjQueryMobilecreatesfromthiskindofelementwillnavigatetothe
appropriatecategorypage.Asanaddedbonus,jQueryMobileproperlycentersgroupsofbuttons
createdfromaelements,soIdon’thavetoworryaboutexplicitlysettingthewidthofthecontaining
fieldsetelement.
TipNoticethatIhaveusedthecssbindingtoapplytheui-btn-activeclasstothebuttonwhentheselected
categorymatchesthecategorythatbuttonrepresents.ThisisthejQueryMobileCSSclassthatisusedwhena
buttonisactive,andapplyingthisclasscreatesthebluehighlightingthatIhadinthepreviousversionofthe
mobilewebapp.DiggingaroundinthetoolkitCSSisn’tideal,butsometimesthereisnoalternative.
MappingPageNamestoRoutes
SothatIcanreusemyJavaScriptcodeforhandlingroutes,Iwanttousethesameroutenamesasinthe
desktopversion.ThisisaproblembecauseoftherestrictionsonpagenamesthatjQueryMobile
enforces.Togetaroundthis,IhaveaddedaroutethatmapsbetweentheroutesthatjQueryMobile
requiresandtheroutesIreallywant:
...
hasher.initialized.add(crossroads.parse, crossroads);
hasher.changed.add(crossroads.parse, crossroads);
hasher.init();
crossroads.addRoute("category/:newCat:", function(newCat) {
cheeseModel.selectedCategory(newCat ||
cheeseModel.products[0].category);
});
crossroads.addRoute("{shortCat}", function(shortCat) {
$.each(cheeseModel.products, function(index, item) {
if (item.shortName == shortCat) {
crossroads.parse("category/" + item.category);
}
});
});
crossroads.parse(location.hash.slice(1));
...
TheURLfragmentchangeswhentheuserclicksoneoftheaelementstonavigatetoanewcategory.
Thehasherlibrarydetectsthischangeandpassesonthenewhashtothecrossroadsroutingengine.The
224
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
jQueryMobileURLmatchesthehighlightedroute,andIenumeratetheproductsintheviewmodelto
findtheonethathasamatchingshortNamevalue.Iusethecategorypropertyoftheproducttocreatethe
kindofURLthatthedesktopversionusesandcallthecrossroads.parsemethodtohaveitmatched
againsttheapplicationroutes.ThistechniqueallowsmetobridgebetweenthejQueryMobileURLsand
routesIwant,allowingmetopreserverouteconsistencyacrossallversionsofmywebapp.Thisisn’ta
bigdealwithmysimpleexampleroutes,butthisbecomesausefultrickifyouhaveanexternal
JavaScriptfilefullofJavaScriptcodethatisexecutedwhenURLsarematched.
ExplicitlyChangingPages
Thelastchangerelatestothedata-categoryattributethatIaddedtothepagedivelements.Whenthe
userswipesthescreenorusesoneofthelandscapenavigationbuttons,theadvanceCategoryfunctionis
called,andthevalueoftheselectedCategoryitemintheviewmodelisupdated.However,updatingthe
viewmodeldoesn’tautomaticallycausejQueryMobiletonavigatetothepagefortheselectedcategory.
Toaddressthis,Ihaveaddedacalltothemobile.changePagemethod.ThismethodwillacceptaURLto
navigatetoorajQueryobjectastheelementtodisplay:
$('button.left, button.right').live("click", function(e) {
e.preventDefault();
advanceCategory(e, $(e.target).hasClass("left") ? "left" : "right");
$.mobile.changePage($('div[data-category="'
+ cheeseModel.selectedCategory() + '"]'));
})
Iusethedata-categoryitemtoselectthepageelementforthenewselectedCategoryvaluewithout
havingtoiteratethroughtheproducts.Withthissmalladdition,IcanrelyonthesameadvanceCategory
codethatIuseinthedesktopversionofthewebappbutgetthebenefitsofthejQueryMobilepage
model.
AddingtheFinalChrome
ThereisjustonefinalchangethatIwanttomaketotheCheeseLuxmobileapp.Atonelevel,itisan
entirelytrivialchange,butitdoesalsoallowmetodemonstrateanimportantbehavioralquirkthat
jQueryMobiledisplays.
jQueryMobileplaysaslidinganimationwhenanewpageisdisplayed.Bydefault,thepageslidesin
fromtheright.ThechangethatIwanttomakeistohavethenewpageslideinfromtheleftwhenthe
userpressestheleftlandscapenavigationbuttonorpressesoneoftheportrait/largescreenbuttonsfora
categorythatappearsintheviewmodelbeforethecurrentcategory.
ThejQueryMobilechangePagemethodacceptsanoptionalconfigurationobject.Oneoftheobject
propertiesthatjQueryMobilerecognizesisreverse.Whenthevalueofthispropertyistrue,thepage
appearsfromtheleft.Thedefaultvalue,false,causesthenewpagetoappearfromtheright.
Fortheportraitnavigationbuttons,Ihaveaddedafunctiontoutils.jscalledgetIndexOfCategory.
Thisfunction,whichisshowninListing8-11,enumeratesthroughtheviewmodeldatatofindtheindex
ofaspecifiedfullorshortcategoryname.
225
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
Listing8-11.ThegetIndexOfCategoryFunction
function getIndexOfCategory(category) {
var result = -1;
for (var i = 0; i < cheeseModel.products.length; i++) {
if (cheeseModel.products[i].category == category ||
cheeseModel.products[i].shortName == category) {
result = i;
break;
}
}
return result;
}
Listing8-12showsthechangesinmobile.htmltomakeuseofthisfunction.
Listing8-12.ManagingPageTransitionAnimationDirection
<script>
var cheeseModel = {};
detectDeviceFeatures(function(deviceConfig) {
cheeseModel.device = deviceConfig;
checkForVersionPreference();
$.getJSON("products.json", function(data) {
cheeseModel.products = data;
enhanceViewModel();
$(document).ready(function() {
ko.applyBindings(cheeseModel);
$('*.deferred').each(function(index, elem) {
ko.applyBindings(cheeseModel, elem);
});
$('button.left, button.right').live("click", function(e) {
e.preventDefault();
advanceCategory(e, $(e.target).hasClass("left") ? "left" : "right");
$.mobile.changePage($('div[data-category="'
+ cheeseModel.selectedCategory() + '"]'),
{reverse: $(e.target).hasClass("left")});
})
$('a[data-role=button]').click(function(e) {
e.preventDefault();
var cIndex = getIndexOfCategory(cheeseModel.selectedCategory());
var newIndex = getIndexOfCategory(this.hash.slice(1));
$.mobile.changePage(this.hash, {reverse: cIndex > newIndex});
});
$.mobile.initializePage();
226
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
hasher.initialized.add(crossroads.parse, crossroads);
hasher.changed.add(crossroads.parse, crossroads);
hasher.init();
crossroads.addRoute("category/:newCat:", function(newCat) {
cheeseModel.selectedCategory(newCat ||
cheeseModel.products[0].category);
});
crossroads.addRoute(":shortCat:", function(shortCat) {
$.each(cheeseModel.products, function(index, item) {
if (item.shortName == (shortCat ||
cheeseModel.products[0].shortName)) {
crossroads.parse("category/" + item.category);
}
});
});
});
});
</script>
});
crossroads.parse(location.hash.slice(1));
IjustneededtoprovidetheoptionalargumenttothechangePagemethodtomakethehorizontal
buttonswork.Fortheaelements,Idecidedtohandletheclickevent,figureoutthetransitiondirection,
andcallthechangePagemethoddirectly.ThereareotherwaysofdoingthisinjQueryMobile,butthisis
thesimplestandmostdirect.
TheimportantjQueryMobilecharacteristicIwantedtodemonstraterelatestothewaythatinternal
URLsaremanaged.jQueryMobilewillnavigatetotheURLfortheentiredocumentratherthanthe
specificpageifyouusethechangePagemethodtonavigatetotheURLthatrepresentsthefirstpagein
thedocument.Forexample,ifyoucallchangePage('#British'),jQueryMobilewillnavigateto
cheeselux.com/mobile.htmlandnotcheeselux.com/mobile.html#British.
Tocaterforthis,IneedtochangetheroutethatmapsbetweenthejQueryMobile–friendlyfragment
URLsandtheroutessharedwiththedesktopversionofthewebapp,likethis:
crossroads.addRoute(":shortCat:", function(shortCat) {
$.each(cheeseModel.products, function(index, item) {
if (item.shortName == (shortCat || cheeseModel.products[0].shortName)) {
crossroads.parse("category/" + item.category);
}
});
});
Imadethesegmentoptional,ratherthanvariable(IexplainthedifferenceinChapter4),andifthere
isnocategorynameprovidedaspartoftheURL,Iassumethatthefirstcategoryintheviewmodel
shouldbeused.Thisisasimplechangeformywebapp,butifyouaremappingcomplexsetsofroutes,
youmustensurethatyousetdefaultsforalloftheroutesegmentsthatareexpectedandwouldusually
beprovidedbythedesktopversion.
227
www.it-ebooks.info
CHAPTER8CREATINGMOBILEWEBAPPS
Summary
Inthischapter,IcreatedasolidmobileimplementationofmyCheeseLuxwebapp.Ishowedyouthe
importanceofadoptingthenavigationmodelprovidedbythemobiletoolkityouareusingandvarious
approachesforintegratingthecorefeaturesofaprofessional-levelwebapp,suchasrouting,view
models,anddatabindings.Mobilewidgettoolkitsusuallyrequiresometweaksandtrickstogetthemto
playnicelywithprowebapps,buttheresultisworthfiguringoutsolutionstothewrinklesthatarise.In
thenextchapter,Ishowyoudifferenttechniquesforimprovingthewayyouwriteandpackageyour
JavaScriptcode.
228
www.it-ebooks.info
CHAPTER 9
Writing Better JavaScript
Inthischapter,IexplainsomeofthetechniquesIusetocreatebetterJavaScript.Thisisnotalanguage
guide,andIwon’tbedemonstratinganycodehacksortweaks.Mycodingpreferencesareyour
maintenancenightmares,andviceversa.Ihaveseenotherwisemild-manneredpeopleendupina
screamingmatchoverthe“right”waytocode,andIdon’tseethepointinlecturingyouwhenIhavea
fairfewbadhabitsmyself.
Instead,IamgoingtoshowyousomeofthetechniquesIusetomakemycodeeasierforother
programmersandprojectstouse.Mostlarge-scalewebappshaveateamofprogrammers,andsharing
codebecomesimportant.
Ihavebeendumpingusefulfunctionsintotheutils.jsfilethroughoutthisbook.ThisishowItend
towork,withageneralkitchen-sinkfilewhereIputfunctionsthatIexpecttorepeatedlyuse.Forthis
book,usingutils.jsletmespendmoretimeineachchapteronthetopicsathandwithouthavingto
spendpageslistingcodethatIdefinedinapreviouschapter.Italsoletmedemonstratetheideaofusing
acoresetofcommonfunctionswhencreatingdesktopandmobileversionsofthesamewebapp.
Theproblemwithjustdumpingfunctionsintoafileinthiswayisthattheybecomehardtomanage
andmaintainand,asI’llexplainshortly,difficultforotherstointegrateintotheirprojects.Forthis
reason,Irevisitmykitchen-sinkfilewhenIhavereachedapointinaprojectwherethebasic
functionalityisstableandIhaveagoodfeelforthewaythatdifferentfeaturesfittogether.Atthispoint,
andnotbefore,Istarttoreworkthecodeintomodulessothatitplaysnicelywithotherlibraries.Inthis
chapter,IshowyouthetechniquesIuseforthis.
OnceIhavetidiedupandmodularizedthecode,Ibeginunittesting.Testingisaverypersonal
thing,andmanytestingproselytizerswillinsistthattestingmustbeginassoonasyoustartcoding,ifnot
sooner.Iunderstandthatpointofview,butIalsoknowthatIdon’teventhinkabouttestinguntilIhave
madeacertainamountofprogresswithaproject.TherenaturallycomesapointwhereIhaveenough
progressandmymindstartstoturntowardconsolidatingandimprovingwhatIhave.
TestingisanothertopiconwhichIamnotgoingtolecture.Myonlyadviceisthatyoushouldbe
honestwithyourself.Testwhenitfeelsright,testuntilyouarehappywithyourcode,andusethe
techniquesandtoolsthatworkforyou.Dowhatisrightforyourproject,andacceptthattestinglaterwill
requiremorecodingchangesandthatnottestingatallmeansyouruserswillhavetofindyourbugsfor
you.
ManagingtheGlobalNamespace
OneofthebiggestproblemswithlargeJavaScriptprojectsisthelikelihoodofanamingcollision,where
tworegionsofcodeusethesameglobalvariablenamesfordifferentpurposes.Aglobalvariableisone
thatexistsoutsideafunctionorobject.JavaScriptmakestheseavailablethroughoutyourweb
applicationsothataglobalfunctiondefinedinaninlinescriptelementorexternalJavaScriptfileis
229
www.it-ebooks.info
CHAPTER9WRITINGBETTERJAVASCRIPT
availabletoeveryotherscriptelementandJavaScriptfileyouuse.Whenaglobalfunctionorvariableis
created,itissaidtoresideintheglobalnamespace.
Forsmallapplications,thisisausefulfeature;itmeansthatyoucanjustpartitionyourcodeandrely
onthebrowsertomergeittogetherwhentheapplicationisloaded.Thisiswhatallowsmyutils.jsfile
towork:thebrowserloadsallofthefunctionsinmyfileandmakesthemavailableviaglobalvariables.I
don’tneedtoknowwherethemapProductsfunctionisdefinedtouseit;itisautomaticallyavailable.
Theproblemcomeswhenyouusecodethathasfunctionsandvariableswiththesamenamesthat
youhaveused.AllsortsofproblemswillariseifIuseaJavaScriptlibrarythatdefinesamapProducts
function.ThemapProductscontainedinthefilethatisloadedlastistheonethatwillwin,andanycode
thatwasexpectingtheotherversionisgoingtobesurprised.
Whatcanbeausefultrickinasmallwebappbecomesamaintenancenightmareasaweb
applicationgrowsinsizeandcomplexity.Itsoonbecomeshardtothinkupmeaningfulnamesthatare
notalreadyinuse,andthelikelihoodofcollisionincreasessharply.Inthesectionsthatfollow,Idescribe
someusefultechniquesthatwillhelpyouavoidnamingcollisionsbystructuringyourcodeandreducing
thenumberofglobalvariablesthatarecreatedasaconsequence.
AVOIDING IMPLIED GLOBAL VARIABLES
Acommoncauseofglobalvariablesistoassignvaluestovariablesthathavenotbeendefinedusingthe
varkeyword.JavaScriptinterpretsthisasarequesttocreateaglobalvariable:
...
(function() {
var var1 = "my local variable";
var2 = "my global variable";
})();
...
Inthislisting,thevariablevar1existsonlywithinthescopeofthefunctionthatdefinesit,butvar2is
definedintheglobalnamespace.Thiscanbeausefulfeaturewhenusedcarefullyanddeliberately,
allowingyoucontroloverwhichvariablesareexportedglobally,butusuallythissituationarisesthrough
errorratherthanintention.Ihaveshownthisinaself-executingfunction,butitcanhappeninanyfunction
thatdefinesvariableswithoutthevarkeyword.
DefiningaJavaScriptNamespace
Thefirsttechniqueistoemploynamespaces,whichlimitthescopeofvariablesandfunctions.Youwill
befamiliarwithnamespacesifyouhaveusedalanguagelikeJavaorC#.JavaScriptdoesn’thavea
namespacelanguageconstructlikethoselanguages,butyoucancreatesomethingthatsolvesthe
problembyrelyingonthewaythatJavaScriptscopesobjects.Listing9-1showshowthisisdone.
Listing9-1.DefiningaJavaScriptNamespace
var cheeseUtils = {};
cheeseUtils.mapProducts = function(func, data, indexer) {
$.each(data, function(outerIndex, outerItem) {
$.each(outerItem[indexer], function(itemIndex, innerItem) {
230
www.it-ebooks.info
CHAPTER9WRITINGBETTERJAVASCRIPT
});
}
func(innerItem, outerItem);
});
cheeseUtils.composeString = function(bindingConfig ) {
var result = bindingConfig.value;
if (bindingConfig.prefix) { result = bindingConfig.prefix + result; }
if (bindingConfig.suffix) { result += bindingConfig.suffix;}
return result;
}
Tocreatethenamespaceeffect,Icreateanobjectandthenassignmyfunctionsandvariablesas
propertieswithinit.Thismeansthattoaccessthesefunctionselsewhere,Ihavetousethenameofthe
objectasaprefix,likethis:
cheeseUtils.mapProducts(function(item) {
if (item.id == id) { item.quantity(0); }
}, cheeseModel.products, "items");
Tobeclear,thisisn’tarealnamespacebecauseJavaScriptdoesn’tsupportthem;itjustlooksand
actsalittlebitlikeone.Butitisenoughtoreducepollutionoftheglobalnamespace,inthatIhavetaken
twofunctionsoutofthesharedcontextandreplacedthemwithasingleobjectname,cheeseUtils.
Thereisstillariskofnamecollision,soitisimportanttoselectanamefortheobjectthatisspecific
toyourprojectorareaoffunctionality.Youcannestnamespacesbynestingobjects,creatingahierarchy
thatmustbenavigatedinordertouseyourcode.Listing9-2showsanexample.
TipTosavespace,Iwon’tlistallofthefunctionsthatareintheutils.jsfile.I’lljustpicksomerepresentative
samplestodemonstratethedifferenttechniques.
Listing9-2.CreatingNestedNamespaces
if (!com) {
var com = {};
}
com.cheeselux = {};
com.cheeselux.utils = {};
com.cheeselux.utils.mapProducts = function(func, data, indexer) {
$.each(data, function(outerIndex, outerItem) {
$.each(outerItem[indexer], function(itemIndex, innerItem) {
func(innerItem, outerItem);
});
});
}
com.cheeselux.utils.composeString = function(bindingConfig ) {
var result = bindingConfig.value;
231
www.it-ebooks.info
CHAPTER9WRITINGBETTERJAVASCRIPT
}
if (bindingConfig.prefix) { result = bindingConfig.prefix + result; }
if (bindingConfig.suffix) { result += bindingConfig.suffix;}
return result;
InthislistingIhaveusedaprettystandardapproachtonamespaces,whichistousethestructureof
mydomainnamebutinreverse.However,sincecomislikelytobeusedbyotherlibrariesfollowingthe
sameapproach,thenIchecktoseewhetherithasbeendefinedalreadybeforedoingsomyself.Idon’t
havetodothisforthecheeseluxpartbecauseIamtheownerofthecheeselux.comdomainandthereis
littlechanceofcollision.
Referringdirectlytofunctionsinanestednamespacecanleadtoverbosecode.WhenIusethecode
inanestednamespaces,Itendtoaliastheinnermostobjecttoalocalvariable,likethis:
var utils = com.cheeselux.utils;
ThiscreatesalooseequivalenttotheimportorusingstatementsdefinedbyJavaandC#(albeit
withouttheisolationfeaturesthatthoseotherlanguagessupport).
Ilikeusingnestednamespaces,probablybecauseItendtowritemyserver-sidecodeinC#,which
encouragesthesameapproach.Tomakecreatingthenamespacessimpler,Irelyonthefactthatglobal
variablesareactuallydefinedaspropertiesonthewindowbrowserobject.Thismakesiteasytocreate
variablesbynamewithoutrelyinginthedreadedevalfunction,asListing9-3shows.
Listing9-3.CreatingNestedNamespacesUsingaFunction
createNamespace("com.cheeselux.utils");
function createNamespace(namespace) {
var names = namespace.split('.');
var obj = window;
for (var i = 0; i < names.length; i++) {
if (!obj[names[i]]) {
obj = obj[names[i]] = {};
} else {
obj = obj[names[i]];
}
}
};
com.cheeselux.utils.mapProducts = function(func, data, indexer) {
$.each(data, function(outerIndex, outerItem) {
$.each(outerItem[indexer], function(itemIndex, innerItem) {
func(innerItem, outerItem);
});
});
}
com.cheeselux.utils.composeString = function(bindingConfig) {
var result = bindingConfig.value;
if (bindingConfig.prefix) { result = bindingConfig.prefix + result; }
if (bindingConfig.suffix) { result += bindingConfig.suffix;}
return result;
}
232
www.it-ebooks.info
CHAPTER9WRITINGBETTERJAVASCRIPT
ThecreateNamespacefunctiontakesanamespaceasanargumentandbreaksitintosegments.The
objectthatrepresentseachsegmentiscreatedonlyifitdoesn’talreadyexist,whichmeansthatIdon’t
collidewithanyoneelse’suseofcomorwithothercom.cheeselux.*namespacesthatIcreateinseparate
JavaScriptfilesformyproject.
■TipCreatingseparatefilesisentirelyoptional.Youcandefinemultiplenamespacesinasinglefileifyouprefer.
Theadvantageofasinglefileisthatthebrowserhastomakeonlyonerequesttogetallofyourcode.Ifyoudo
likeusingmultiplefiles,thenyoucansimplyconcatenatethemintoonewhenyoureleaseyourwebapp.
Icangoonestepfurtherandmakethenamespaceitselfmoreeasilyconfigurable,asListing9-4
demonstrates.ThismakesitmucheasiertorenamemynamespaceifthereisaconflictandmeansthatI
canselectashorternametosavemyselfsometyping.
Listing9-4.MakingNamespacesEasilyConfigurable
function createNamespace(namespace) {
var names = namespace.split('.');
var obj = window;
for (var i = 0; i < names.length; i++) {
if (!obj[names[i]]) {
obj = obj[names[i]] = {};
} else {
obj = obj[names[i]];
}
}
return obj;
};
var utilsNS = createNamespace("cheeselux.utils");
utilsNS.mapProducts = function(func, data, indexer) {
$.each(data, function(outerIndex, outerItem) {
$.each(outerItem[indexer], function(itemIndex, innerItem) {
func(innerItem, outerItem);
});
});
}
utilsNS.composeString = function(bindingConfig) {
var result = bindingConfig.value;
if (bindingConfig.prefix) { result = bindingConfig.prefix + result; }
if (bindingConfig.suffix) { result += bindingConfig.suffix;}
return result;
}
IhaveupdatedthecreateNamespacefunctionsothatitreturnsthenamespaceobjectitcreates.This
allowsmetocreateanamespaceandassigntheresultasavariable,whichIcanthenusetoadd
233
www.it-ebooks.info
CHAPTER9WRITINGBETTERJAVASCRIPT
functionstothenamespace.IfIneedtochangethenameofthenamespace,thenIhavetodoitonlyin
thecalltothecreateNamespacemethod(and,ofcourse,inanycodethatreliesonmyfunctions).Inthis
example,Ihaveshortenedmynamespacebydroppingthecomprefix.Theoddsoftherebeingaconflict
arestillprettyslim,butifitdoesarise,itisasimpleenoughmattertoadapt.
UsingSelf-executingFunctions
OnedrawbackoftheprevioustechniqueisthatIendupcreatinganotherglobalvariable,utilsNS.This
isstillabetterapproachthandefiningallofmyvariablesglobally,butitissomewhatself-defeating.
Icanaddressthisbyusingaself-executingfunction.Thistechniquereliesonthefactthata
JavaScriptvariabledefinedwithinafunctionexistsonlywithinthescopeofthatfunction.Theselfexecutingaspectmeansthatthefunctionrunswithoutbeingexplicitlyinvokedfromanotherpartofthe
code.Thetrickistodefineafunctionandhaveitexecuteimmediately.Itiseasiertoseethestructureof
aself-executingfunctionwhenthereisn’tanyothercode:
(function() {
...statements go here...
})();
Tomakeafunctionself-execute,youwrapitinparenthesesandthenapplyanotherpairof
parenthesesattheend.Thisdefinesandcallsthefunctioninasinglestep.Anyvariablesdefinedwithin
thefunctionaretidiedupafterthefunctionhasfinishedexecutinganddon’tendupintheglobal
namespace.Listing9-5showshowIcanapplythistomyutilityfunctions.
Listing9-5.UsingaSelf-executingFunctiontoDefineNamespaces
(function() {
function createNamespace(namespace) {
var names = namespace.split('.');
var obj = window;
for (var i = 0; i < names.length; i++) {
if (!obj[names[i]]) {
obj = obj[names[i]] = {};
} else {
obj = obj[names[i]];
}
}
return obj;
};
var utilsNS = createNamespace("cheeselux.utils");
utilsNS.mapProducts = function(func, data, indexer) {
$.each(data, function(outerIndex, outerItem) {
$.each(outerItem[indexer], function(itemIndex, innerItem) {
func(innerItem, outerItem);
});
});
}
utilsNS.composeString = function(bindingConfig) {
var result = bindingConfig.value;
234
www.it-ebooks.info
CHAPTER9WRITINGBETTERJAVASCRIPT
}
})();
if (bindingConfig.prefix) { result = bindingConfig.prefix + result; }
if (bindingConfig.suffix) { result += bindingConfig.suffix;}
return result;
Theonlyglobalvariablethatisleftisthecheeseluxnamespaceobject.Myfunctionsaredefined
withinthecheeselux.utilsnamespace,andmyutilsNSvariableistidiedupbythebrowserwhenthe
self-executingfunctionhasfinished.
Consumingafunctiondefinedinthiswayisstilljustamatterofreferringtothefunctionviathe
namespace,likethis:
cheeselux.utils.mapProducts(function(item) {
if (item.id == id) { item.quantity(0); }
}, cheeseModel.products, "items");
CreatingPrivateProperties,Methods,andFunctions
InJavaScript,everyproperty,method,andfunctionisavailableforusefromanyotherpartofthecode
thatcreatesorcanaccessthem.Thismakesitdifficulttoindicatewhichmembersareintendedforuse
byothersandwhicharetheinternalimplementationsoffeatures.
Thedifferenceisimportant;youwanttobeabletochangetheinternalimplementationtofixbugs
oraddnewfeatureswithouthavingtoworryifsomeonehascreatedadependencythatyouweren’t
expecting.Anyoneusingyourcodeneedstoknowwhatpropertiesandmethodstheycanrelyonnotto
changewithoutduenotice.JavaScriptdoesn’thaveanykeywordsthatcontrolaccess(suchaspublic
andprivate,whicharefoundinotherlanguages)andsoweneedtofindalternativeapproachesto
addressthisshortfall.
Thesimplestsolutiontothisproblemistoadoptanamingconventionthatmakesitclearthatsome
propertiesandmethodsarenotintendedforpublicuse.Themostwidelyadoptedconventionisto
prefixprivatenameswithanunderscorecharacter(_).
MycomposeStringfunctionisanidealcandidatetobeprivate.Iusethisfunctiononlyinmycustom
databindings,andIwanttobefreetochangeeveryaspectofthisfunction(includingitsveryexistence)
asmybindingsevolve.Thereisnoreasonforanyotherprogrammertodependonthisfunction,evenif
theyusemybindings.Listing9-6showstheunderscorenamingstyleappliedtothisfunctionandthe
databindingsthatrelyonit.
Listing9-6.ApplyingaNamingConventiontoDenoteaPrivateFunction
(function() {
function createNamespace(namespace) {
var names = namespace.split('.');
var obj = window;
for (var i = 0; i < names.length; i++) {
if (!obj[names[i]]) {
obj = obj[names[i]] = {};
} else {
obj = obj[names[i]];
}
}
return obj;
235
www.it-ebooks.info
CHAPTER9WRITINGBETTERJAVASCRIPT
};
var utilsNS = createNamespace("cheeselux.utils");
utilsNS.mapProducts = function(func, data, indexer) {
$.each(data, function(outerIndex, outerItem) {
$.each(outerItem[indexer], function(itemIndex, innerItem) {
func(innerItem, outerItem);
});
});
}
utilsNS._composeString = function(bindingConfig) {
var result = bindingConfig.value;
if (bindingConfig.prefix) { result = bindingConfig.prefix + result; }
if (bindingConfig.suffix) { result += bindingConfig.suffix;}
return result;
}
})();
ko.bindingHandlers.formatAttr = {
init: function(element, accessor) {
$(element).attr(accessor().attr, cheeselux.utils._composeString(accessor()));
},
update: function(element, accessor) {
$(element).attr(accessor().attr, cheeselux.utils._composeString(accessor()));
}
}
ko.bindingHandlers.formatText = {
update: function(element, accessor) {
$(element).text(cheeselux.utils._composeString(accessor()));
}
}
...
Adoptinganamingconventiondoesn’tpreventothersfromusingprivatemembers,butitdoes
signalthatdoingsoisagainstthewishesofthedeveloperandthattheproperty,method,orfunctionis
subjecttochangewithoutnotice.Itisimportanttouseanamingconventionthatiswidelyadopted
(suchastheunderscore)orthatisimmediatelyobvious(suchasprefixingnameswiththeword
private).
Analternativeapproachistolimitthescopeofprivatefunctionssothattheyarenotdefinedaspart
ofthenamespace.Thispreventsthefunctionfrombeingaccessedelsewhereinthewebapp,butit
meansthatallofthedependenciesonthatfunctionmustappearwithinthesameself-executing
function,whichisn’talwayspractical.Listing9-7showshowthisapproachworks.
236
www.it-ebooks.info
CHAPTER9WRITINGBETTERJAVASCRIPT
Listing9-7.UsingaSelf-executingFunctiontoKeepaFunctionPrivate
(function() {
function createNamespace(namespace) {
var names = namespace.split('.');
var obj = window;
for (var i = 0; i < names.length; i++) {
if (!obj[names[i]]) {
obj = obj[names[i]] = {};
} else {
obj = obj[names[i]];
}
}
return obj;
};
var utilsNS = createNamespace("cheeselux.utils");
utilsNS.mapProducts = function(func, data, indexer) {
$.each(data, function(outerIndex, outerItem) {
$.each(outerItem[indexer], function(itemIndex, innerItem) {
func(innerItem, outerItem);
});
});
}
function _composeString(bindingConfig) {
var result = bindingConfig.value;
if (bindingConfig.prefix) { result = bindingConfig.prefix + result; }
if (bindingConfig.suffix) { result += bindingConfig.suffix;}
return result;
}
ko.bindingHandlers.formatAttr = {
init: function(element, accessor) {
$(element).attr(accessor().attr, _composeString(accessor()));
},
update: function(element, accessor) {
$(element).attr(accessor().attr, _composeString(accessor()));
}
}
ko.bindingHandlers.formatText = {
update: function(element, accessor) {
$(element).text(_composeString(accessor()));
}
}
})();
237
www.it-ebooks.info
CHAPTER9WRITINGBETTERJAVASCRIPT
The_composeStringfunctionisneverdefinedaspartofthelocalorglobalnamespacesandis
availableonlyforuseinthesameenclosingself-executingfunction.Thistechniqueworksbecause
JavaScriptsupportsclosures,whichbringsvariablesandfunctionsinscopeevenwhentheyaredefined
inthismanner.
ManagingDependencies
Packagingupmyfunctionsintonamespacesmakesthemmoremanageableandhelpscleanupthe
globalnamespace,butthereisstillonemajorissue:dependenciesonotherlibraries.Inthesectionsthat
follow,Ishowyouatechniqueformanagingdependenciesinlibrariesthatisstartingtogainin
popularityandthatyoucanusetomakeyourcodeeasiertoshareandeasiertoworkwith.
UnderstandingAssumedDependencyProblems
TherearetwokindsofdependencyinanexternalJavaScriptfilesuchasutils.js.Thefirstkindisan
assumeddependency,whereIjustusethefunctionalityofalibraryandassumeitwillbeavailable.Ihave
donethisalotinutils.js,especiallywithjQuery.Anassumeddependencyplacesresponsibilityonthe
HTMLdocumentthatusesaJavaScriptfiletoloadtherequiredlibrariesandtodosobeforemycodeis
executed.ThemapProductsfunctionisagoodexampleofanassumeddependency:
utilsNS.mapProducts = function(func, data, indexer) {
$.each(data, function(outerIndex, outerItem) {
$.each(outerItem[indexer], function(itemIndex, innerItem) {
func(innerItem, outerItem);
});
});
}
ThisfunctionassumesthatthejQuery$.eachmethodwillbeavailable.Ifyouwanttousethis
function,thenyouneedtoensurethatjQueryisloadedandreadybeforeyoucallmapProducts.Listing
9-8showsaverysimplejQueryMobilewebappthatmakesuseofthemapProductsfunction.Thereis
nothingnewinthistinywebapp,butIamgoingtouseittodemonstratedifferentdependencyissues
andsolutionsinthesectionsthatfollow.
Listing9-8.ASimpleWebAppThatUsesaJavaScriptFileThatContainsanAssumedDependency
<!DOCTYPE html>
<html>
<head>
<title>CheeseLux</title>
<link rel="stylesheet" type="text/css" href="jquery.mobile-1.0.1.css"/>
<link rel="stylesheet" type="text/css" href="styles.mobile.css"/>
<script src="jquery-1.7.1.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).bind("mobileinit", function() {
$.mobile.autoInitializePage = false;
});
</script>
<script src="jquery.mobile-1.0.1.js" type="text/javascript"></script>
<script src='knockout-2.0.0.js' type='text/javascript'></script>
<script src='modernizr-2.0.6.js' type='text/javascript'></script>
238
www.it-ebooks.info
CHAPTER9WRITINGBETTERJAVASCRIPT
<script src='utils.js' type='text/javascript'></script>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script>
var cheeseModel = {
selectedCount: ko.observable(0)
};
$.getJSON("products.json", function(data) {
cheeseModel.products = data;
$(document).ready(function() {
ko.applyBindings(cheeseModel);
$.mobile.initializePage();
$('a[data-role=button]').click(function(e) {
var count = 0;
cheeselux.utils.mapProducts(function(inner, outer) {
if (outer.category == e.currentTarget.id) {
count++;
}
}, cheeseModel.products, "items")
cheeseModel.selectedCount(count);
});
});
});
</script>
</head>
<body>
<div data-role="page" id="page1" data-theme="a">
<fieldset class="middle" data-role="controlgroup" data-type="horizontal"
data-bind="foreach: products">
<a data-role="button" data-bind="text: category, attr: {id: category}"></a>
</fieldset>
<div class="middle results" data-bind="visible: selectedCount">
There are <span data-bind="text: selectedCount"></span>
cheeses in this category
</div>
</div>
</body>
</html>
NoteThisisanentirelyuselesswebappinitsownright.Abuttonisdisplayedforeachcheesecategory,and
clickingthebuttondisplaysthenumberofcheeseswithinthatcategory.Ignore,ifyouwill,thefactthatthereare
easierwaystoobtainthisinformationthanusingthemapProductsmethodandthattherearethreecheesesin
everysinglecategory.Thiswitlesswebappisperfectfordemonstratingthekeyaspectsofdependency
management.
239
www.it-ebooks.info
CHAPTER9WRITINGBETTERJAVASCRIPT
UnderstandingDirectlyResolvedDependencies
ThetinywebappworksbecausejQueryhasbeenloadedlongbeforeIcallthemapProductsfunction.The
situationwouldbedifferentifIrewrotethewebapptouseadifferenttoolkit.Mostprogrammersdothe
samethingwhentheyfirstunderstandthatassumeddependenciesareaproblem:theyassumecontrol
ofthesituationandtakedirectactiontofixit.Listing9-9showsatypicalsolution.
Listing9-9.TakingDirectActiontoResolveAssumedDependencies
(function() {
function createNamespace(namespace) {
...code removed for brevity...
};
var utilsNS = createNamespace("cheeselux.utils");
Modernizr.load({
load: 'jquery-1.7.1.js',
complete: function() {
utilsNS.mapProducts = function(func, data, indexer) {
$.each(data, function(outerIndex, outerItem) {
$.each(outerItem[indexer], function(itemIndex, innerItem) {
func(innerItem, outerItem);
});
});
}
}
})
...code removed for brevity...
})();
Inthislisting,IhavetakenresponsibilityforresolvingmydependencyonjQuerybyusing
ModernizrtoloaditbeforecreatingmymapProductsfunction.(TheloadpropertyinaModernizr.load
objectspecifiesthattheJavaScriptfileshouldalwaysbeloaded.)
Indoingthis,Ihavetransformedanassumeddependencyintoadirectlyresolveddependency.A
directlyresolveddependencyiswhenIrelyonanotherJavaScriptlibraryandItakedirectactiontomake
mycodework,usuallybyloadingthelibrarymyself.
UnderstandingtheProblemsCausedbyResolvingaDependency
Directlyresolvingadependencycausesalotofproblems.First,Icreatedanassumeddependencyon
ModernizrtoensurethatjQueryisloaded,whichisn’tahugestepforward.ButtherealdamageisthatI
havemadesurethatthemapProductsfunctionworks;however,indoingso,Ihaveunderminedthe
stabilityofthewebappitself.
Toseetheproblem,loadthewebapp,andreloadthepageafewtimes.Therearetwoissues.Ifthe
webappworks,youhaveencounteredjusttheleastseriousone,whichisthatthejQuerylibraryhas
beenloadedtwice.Youcanseethisinthebrowserdevelopertoolsorintheconsoleoutputfromthe
Node.jsserverthatprintsouteachURLthatisrequested.Hereisthelistoffilesloadedbythewebappas
reportedbytheserver,withannotationstohighlightthetwoloadsforjQuery:
240
www.it-ebooks.info
CHAPTER9WRITINGBETTERJAVASCRIPT
The "sys" module is now called "util". It should have a similar interface.
Ready on port 80
Ready on port 81
GET request for /example.html
GET request for /jquery.mobile-1.0.1.css
GET request for /styles.mobile.css
GET request for /jquery-1.7.1.js
GET request for /jquery.mobile-1.0.1.js
GET request for /knockout-2.0.0.js
GET request for /modernizr-2.0.6.js
GET request for /utils.js
GET request for /products.json
GET request for /jquery-1.7.1.js
GET request for /images/ajax-loader.png
<-- first load
<-- second load
Youcantellwhetheryouhaveencounteredonlythefirstproblembecauseyouwillseethree
buttons,andclickingoneofthemmakesamessageappear.Youknowthatyouhaveencounteredthe
secondproblemifyoujustgetanemptywindow.Figure9-1showsbothoutcomes.
Figure9-1.Thetwooutcomesthatarisefromadirectlyresolveddependency
Thesecondproblemisaracecondition,anditwon’talwaysmanifestitselfwhenyouareloadingall
oftheresourcesfromthewebappfromthelocalmachine.IftheAjaxrequestcompletesafterModernizr
hasloadedthejQuerylibraryandexecutedthecallbackfunction,thenyouwillgettheblankwindow,
andtherewillbeanerrormessageintheJavaScriptconsolelikethis:
Uncaught TypeError: Cannot call method 'initializePage' of undefined
Theexactwordingwillvaryfrombrowsertobrowser,buttheproblemisthatthecallto
$.mobile.initializePagehasfailedbecausethereisno$.mobileobject.Tohelpforcetheproblemto
appear,IhaveaddedaspecialURLtotheNode.jsserverthatintroducesadelayinreturningtheJSON
content.Totriggerthisdelay,changethenameoftheJSONfilerequestedbythegetJSONmethod,as
showninListing9-10.
241
www.it-ebooks.info
1
CHAPTER9WRITINGBETTERJAVASCRIPT
Listing9-10.DeliberatelyIntroducingaDelayintheAjaxRequestfortheJSONData
...
<script>
var cheeseModel = {
selectedCount: ko.observable(0)
};
$.getJSON("products.json.slow", function(data) {
cheeseModel.products = data;
$(document).ready(function() {
ko.applyBindings(cheeseModel);
$.mobile.initializePage();
... code removed for brevity...
});
});
</script>
...
Requestingproducts.json.slowinsteadofproducts.jsonwilladdaone-seconddelaytotheAjax
requestthatwillforcetheAjaxrequesttotakelongerthanModernizrrequirestoloadthejQuerylibrary.
Youcanedittheserver.jsfiletoaddalongerdelayifyoudon’tseetheproblem,butone-second
consistentlycausesthewhitescreenforme.
TipThisispartofwhatmakesthisproblemsonasty;itusuallywon’tappearduringdevelopmentbecausethe
Ajaxrequestwillcompletesoquickly.Unfortunately,itdoesappearindeploymentwhenrequestsaremadeto
busyserversovercongestednetworks.Ifyoueverfindyourselfgettinguserreportsofblankscreensthatyoucan’t
replicate,itisalwaysagoodideatoseewhetheryourlibrariesareself-resolvingdependencies.
HereisthesequenceofeventswhentheAjaxrequestcompletesbeforeModernizrhasloaded
jQuery:
1.
jQueryisloadedbythebrowserfromthescriptelementinexample.html and
setsupthe$shorthandreference.
2.
jQueryMobileisloadedandaddsthemobilepropertytothejQuery$
shorthand.
3.
TheAjaxrequestcompletes,andthe$.mobile.initializePagemethodis
called.
4.
ModernizrloadsthejQuerylibraryagain,whichreplacesthe$shorthandwith
anobjectthatdoesn’thavethejQueryMobilemobileproperty.
242
www.it-ebooks.info
CHAPTER9WRITINGBETTERJAVASCRIPT
Thisisthebest-casescenariowherejQueryisloadedandexecutedtwice,butatleastthewebapp
works.ThesequencechangeswhentheAjaxrequestcompletesafterModernizrhasloadedjQuery:
1.
jQueryisloadedbythebrowserfromthescriptelementinexample.html and
setsupthe$shorthandreference.
2.
jQueryMobileisloadedandaddsthemobilepropertytothejQuery$
shorthand.
3.
ModernizrloadsthejQuerylibraryagain,whichreplacesthe$shorthandwith
anobjectthatdoesn’thavethejQueryMobilemobileproperty.
4.
TheAjaxrequestcompletes,andthe$.mobile.initializePagemethodis
called.
Youcanseetheproblem:thecallto$.mobile.initialPageismadeafterthesecondinstanceof
jQueryhasbeenloadedandthe$shorthandhasbeenredefined,whicherasesthemobileproperty.The
effectisthatloadingjQueryasecondtimehasunloadedjQueryMobileandsothewebappdiesa
horribledeath.Eveninthebest-casescenario,theonlyreasonthatthewebappworksisbecauseitisso
simple;anycalltoajQueryMobilefunctionwillcauseaproblemonceModernizrhascausedthemobile
objecttobedeleted.
TipThereisasecondraceconditioninthissituation.ThemapProductsfunctionisn’tdefineduntilModernizr
hasloadedthejQuerylibrary,whichmeansthatadelayinprocessingtherequest(becausetheserverorthe
networkisbusy)canleadtothecodeintheinlinescriptelementcallingmapProductsbeforeitexists.Iamnot
goingtodemonstratethisissue,butyougettheidea:directlyresolveddependenciesareextremelydangerous.
MakingaBadProblemintoaSubtleBadProblem
Beforemovingtoarealdependencysolution,Iwanttoshowyouacommonattemptatfixingthe
double-loadingproblem:testingtoseewhetherthelibraryisloaded,likethis:
...
Modernizr.load({
test: $.each,
nope: 'jquery-1.7.1.js',
complete: function() {
utilsNS.mapProducts = function(func, data, indexer) {
$.each(data, function(outerIndex, outerItem) {
$.each(outerItem[indexer], function(itemIndex, innerItem) {
func(innerItem, outerItem);
});
});
}
}
})
...
243
www.it-ebooks.info
CHAPTER9WRITINGBETTERJAVASCRIPT
IhaveusedModernizrtotestsomeindicatorthatjQueryhasalreadybeenloadedandusethenope
propertytoloadtheJavaScriptfileifithasn’t.Applyingthistechniquetomytinyexamplewebappwill
makeeverythingwork.Butitisn’tarealsolution,andwhilethenewproblemIcreatedoccursless
frequently,itismuchhardertotrackdown.
TheunderlyingproblemisthatIamstilljusttryingtomakemycodework.Ifutils.jsistheonlyfile
thatusesthistechnique,theneverythingwillbefine,withtheexceptionthatthemapProductsfunction
maynotbedefinedinatimelyenoughmannerifthejQuerylibrarydoesneedtobeloadedandthereisa
delayintherequest.However,ifthistechniqueisusedinmorethanonefile,thenthereisaverysubtle
racecondition.ImaginethattherearetwofilesthatuseModernizrtotestforjQuery:fileA.jsand
fileB.js.Mostofthetime,thesequenceofeventswillbethis:
1.
ThebrowserexecutesthecodeinfileA.js,whichtestsforjQuery.jQuery
hasn’tbeenloaded,soModernizrrequeststhefileandthenexecutesthe
completefunction.
2.
ThebrowserexecutesthecodeinfileB.js,whichtestsforjQuery.jQueryhas
beenloadedviafileA.js,andModernizrexecutesthecompletefunction
withoutneedingtoloadanyfiles.
However,Modernizrrequestsareasynchronous,whichmeansthatthebrowserwillcontinueto
executeJavaScriptcodewhileModernizrwaitsfortheresponsefromtheserver.So,ifthetimingisjust
right,thesequencewillreallybeasfollows:
1.
ThebrowserexecutesthecodeinfileA.js,whichtestsforjQuery.jQuery
hasn’tbeenloaded,soModernizrrequeststhefile.
2.
ThebrowsercontinuestoexecutecodewhileModernizriswaitingandbegins
processingfileB.js.TheModernizrrequestfromfileA.jshasn’tcompleted
yet,sofileB.jscausesModernizrtomakeasecondrequestforthejQueryfile.
3.
ThefileA.jsrequestcompletes,jQueryisloaded,andthefileA.jscomplete
functionisexecuted.
4.
ThefileB.jsrequestcompletes,jQueryisloadedforasecondtime,andthe
fileB.jscompletefunctionisexecuted.
AnypropertiesthatthecompletefunctioninfileA.jsaddstothejQuery$shorthandwillbelost
whenModernizrloadsjQueryagain.Thissequenceoccursinfrequently,butwhenitdoes,itcankillthe
webappbydeletingessentialfunctionalityrequiredinatleastoneoftheJavaScriptfiles.Youmight
thinkthatinfrequentproblemsareacceptable,butinfrequentcanstillbeaseriousissuewhenyourweb
apphasmillionsofusers.
UsingtheAsynchronousModuleDefinition
Theonlyrealwaytoeliminateraceconditionsandduplicatedlibraryloadingistodealwith
dependenciesinacoordinatedway,andthismeanstakingresponsibilityforloadingdependenciesout
ofindividualJavaScriptfilesandconsolidatingthem.ThebestmodelfordoingthisistheAsynchronous
ModuleDefinition(AMD),whichI’llexplainanddemonstrateinthesectionsthatfollow.
244
www.it-ebooks.info
CHAPTER9WRITINGBETTERJAVASCRIPT
DefininganAMDModule
Definingamoduleisprettysimpleandhingesontheuseofthedefinefunction.Listing9-11showshow
Ihavecreatedamoduleinanewfilecalledutils-amd.js.Youdon’thavetoincludeamdinthefilename;
that’sjustmypreferencebecauseIliketomakeitasobviousaspossibletotheconsumersofmycode
thattheyaredealingwithAMD.ProvidingthedefinefunctionistheresponsibilityoftheAMDloader.As
anauthorofAMDmodules,youcanrelyonthedefinefunctionbeingpresentwithouthavingtoworry
aboutwhichloaderisbeingusedorhowthefunctionisimplemented.
Listing9-11.Theutils-amd.jsFile
define(['jquery-1.7.1.js'], function() {
return {
mapProducts: function(func, data, indexer) {
$.each(data, function(outerIndex, outerItem) {
$.each(outerItem[indexer], function(itemIndex, innerItem) {
func(innerItem, outerItem);
});
});
},
composeString: function(bindingConfig) {
var result = bindingConfig.value;
if (bindingConfig.prefix) { result = bindingConfig.prefix + result; }
if (bindingConfig.suffix) { result += bindingConfig.suffix;}
return result;
}
};
});
ThedefinefunctioncreatesanAMDmodule.Thefirstargumentisanarrayofthelibrariesthatthe
codeinthemoduledependson.Thesecondargumentisafunction,knownasthefactoryfunction,that
containsthemodulecode.OnlyoneAMDmodulecanbedefinedinafile,andsinceIliketokeepthe
functionalitydefinedinamodulenarrowlyfocused,myutils-amd.jsfilecontainsjustthemapProducts
andcomposeString functions.(I’llreturntosomeoftheothercodefromutils.jsinawhile.)
AnAMDmodulecanrelyonallofthedeclareddependenciesbeingloadedbeforethefactory
functionisexecuted.Inthiscase,Ihavedeclaredadependencyonjquery-1.7.1.js,andIcanassume
thatthisJavaScriptfilewillbeloadedandjQuerywillbeavailableforusewhenIsetupmymapProducts
andcomposeStringfunctions.Theresultfromthefactoryfunctionisanobjectwhosepropertiesarethe
functionsIwanttoexportforuseelsewhereinthewebapp.AnyvariablesorfunctionsthatIdefineand
thatarenotpartoftheresultobjectwillbetidiedupwhenthefactoryfunctionhasexecutedwithout
pollutingtheglobalnamespace.
TipNoticethatthereisnonamespaceinmymodule.OneofthenicefeaturesofAMDisthatitisuptothe
consumerofmymoduletodecidehowtorefertothefunctionalitythatIdefine,asI’lldemonstrateinthenext
section.
245
www.it-ebooks.info
CHAPTER9WRITINGBETTERJAVASCRIPT
UsinganAMDModule
AMDsolvesthedependencyissuesbyhavingasingleresourceloadertakeresponsibilityforloading
libraries.Thisloaderisresponsibleforexecutingamodule’sfactoryfunctionandensuringthatthe
librariesitreliesonareloadedandreadybeforethishappens.Themainmeansofcommunication
betweenamoduleandtheloaderisthroughthedefinefunction,whichtheloaderisresponsiblefor
implementing.
Bystandardizingtheloadingprocess,thedecisionaboutwhichloadertouseislefttotheconsumer
ofAMDmodules,ratherthantheauthor.So,Idon’thavetoworryaboutresolvingdependencieswhenI
writeanAMDmodule,andIdon’tevenhavetoworryabouthowtheywillbedealtwith.
AlthoughtheAMDformatisgainingpopularity,notallresourceloaderssupportAMD.Thisincludes
Modernizr.load,whichIhavebeenusingtoloadlibrariessofarinthisbook(andtodemonstratewhy
thisisabadideainthischapter).MyfavoriteAMD-awareloaderisrequireJS,whichyoucandownload
fromhttp://requirejs.org.YoucanseehowIhaveappliedrequireJStomytinywebappinListing9-12.
Listing9-12.UsingrequireJStoLoadAMDModules
<!DOCTYPE html>
<html>
<head>
<title>CheeseLux</title>
<link rel="stylesheet" type="text/css" href="jquery.mobile-1.0.1.css"/>
<link rel="stylesheet" type="text/css" href="styles.mobile.css"/>
<script src='require.js' type='text/javascript'></script>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script>
var libs = [
'utils-amd',
'device-amd',
'custombindings-amd',
'jquery-1.7.1.js',
'knockout-2.0.0.js',
'modernizr-2.0.6.js'
];
require(libs, function(utils, device) {
var cheeseModel = {
selectedCount: ko.observable(0)
};
$(document).bind("mobileinit", function() {
$.mobile.autoInitializePage = false;
});
$.getJSON("products.json", function(data) {
cheeseModel.products = data;
device.detectDeviceFeatures(function(deviceConfig) {
cheeseModel.device = deviceConfig;
$(document).ready(function() {
ko.applyBindings(cheeseModel);
246
www.it-ebooks.info
CHAPTER9WRITINGBETTERJAVASCRIPT
requirejs(['jquery.mobile-1.0.1.js'], function() {
$.mobile.initializePage();
});
});
});
$('a[data-role=button]').click(function(e) {
var count = 0;
utils.mapProducts(function(inner, outer) {
if (outer.category == e.currentTarget.id) {
count++;
}
}, cheeseModel.products, "items")
cheeseModel.selectedCount(count);
});
});
});
</script>
</head>
<body>
<div data-role="page" id="page1" data-theme="a">
<fieldset class="middle" data-role="controlgroup" data-type="horizontal"
data-bind="foreach: products">
<a data-role="button" data-bind="text: category, attr: {id: category}"></a>
</fieldset>
<div class="middle results" data-bind="fadeVisible: selectedCount()">
There are <span data-bind="text: selectedCount"></span>
cheeses in this category
</div>
</div>
</body>
</html>
DeclaringDependencies
Thefirstthingtodoisremoveallofthescriptelementsintheheadsectionofthedocumentandreplace
themwithasingleelementthatimportsrequireJS.ThisensuresthatrequireJShasacompleteviewofall
ofthedependenciesinthewebappandthatyoudon’tenduploadingscriptfilestwiceiftheyare
requiredindependentlibraries.
...
<script src='require.js' type='text/javascript'></script>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script>
var libs = [
'utils-amd',
'device-amd',
'custombindings-amd',
'jquery-1.7.1.js',
'knockout-2.0.0.js',
247
www.it-ebooks.info
CHAPTER9WRITINGBETTERJAVASCRIPT
];
'modernizr-2.0.6.js'
require(libs, function(utils, device) {
...
ThemostimportantfeatureofanAMDloaderistherequirefunction,whichisthecounterpartto
define.Therequirefunctiontakestwoarguments:anarrayofmodulesandscriptfilesthatthewebapp
dependsonandacallbackfunctiontoexecutewhentheyareallloaded.Ifindthatdefiningthe
dependencyarrayasavariablemakesmycodemorereadable,butthatispurelyapersonalpreference.
NoteTheAMDmoduletakescareoftheproblemsaroundhowdependenciesareresolved,butitstillrequires
thattheJavaScriptfilesareavailablefromthewebserver.Whensharingyourcodewithothers,youwillstillneed
toletthemknowwhichlibrariesyoudependuponandmakeitclearthatyouareusingAMDandsotheywillneed
anAMDloader.
Noticethatsomeoftheitemsinthedependencyarrayhavea.jssuffixandothersdon’t.Notallof
thedependenciesorawebappwillbewrittenasAMDmodules.IfyoupassrequireJSthenameofa
JavaScriptfile(i.e.,witha.jssuffix),thenitwillloadthefileandexecutethecodeinsideofitjustlike
anyregularresourceloader.
Ifyouomitthe.jssuffix,thenrequireJSassumesyouhavespecifiedanAMDmoduleandacts
accordingly.Itwilladdthe.jssuffixwhenitrequeststhefilefromtheserver,andwhenitreceivesthe
response,itwilllookforthedefinefunctioninordertodiscoverthedependenciesandthefactory
function.
TipByforcingeachfiletocontainonlyonemodule,AMDincreasesthenumberofHTTPrequeststhatare
requiredtogetthescriptsforawebapp.Inthisexample,Ihavegonefromonefile(utils.js)tothree(utilsamd.js,device-amd.js,andcustombindings-amd.js).IwouldhaveendedupwithmoreifIhadproperly
packagedupallofthefunctionsthatutils.jscontained.Toaddressthis,requireJSsupportsaserver-side
optimizerthatwillconcatenatemultipleAMDmodulefilesintoasingleresponse.See
http://requirejs.org/docs/optimization.htmlfordetails.
DealingwithCallbackArguments
ForeachAMDmoduleinthelistpassedtorequire,thereisacorrespondingargumentpassedtothe
callbackfunction.Eachargumentissettotheobjectreturnedbythefactoryfunctioninthemodule.
Thisisanicealternativetonamespaces;theconsumerofthemodulegetstodecidehowtorefertothe
modulefunctionsratherthanthecreator.
Thefirstmoduleinmylistisutils-amd,andthiscorrespondstotheutilargumentinmycallback
function.WhenIwanttousethemapProductsfunctiondefinedbythemodule,Imakeacalllikethis:
248
www.it-ebooks.info
CHAPTER9WRITINGBETTERJAVASCRIPT
utils.mapProducts(function(inner, outer) {
if (outer.category == e.currentTarget.id) {
count++;
}
}, cheeseModel.products, "items")
cheeseModel.selectedCount(count);
IfIlaterstartusingaregularJavaScriptlibrarythatusesutilsasaglobalvariable,Icaneasily
changethewaythatIrefertothecodeintheutils-amdmodulebyrenamingtheargumentforthe
callbackfunction.And,sincethefunctionsarescopedwithinthecontextofthecallbackargument,AMD
modulesdon’tpollutetheglobalnamespaceatall.
So,whyaretherethreeAMDmodulesinthelistbutonlytwocallbackarguments?Theansweris
thatmodulesarenotrequiredtoreturnanobjectiftheydon’tneedtoexportfunctions,andthisisthe
approachIhavetakenwiththecustombindings-amdmodule,whichyoucanseeinListing9-13.
Listing9-13.AnAMDModuleThatDoesn’tExportFunctions
define(['utils-amd', 'jquery-1.7.1.js', 'knockout-2.0.0.js'], function(utils) {
ko.bindingHandlers.formatAttr = {
init: function(element, accessor) {
$(element).attr(accessor().attr, utils.composeString(accessor()));
},
update: function(element, accessor) {
$(element).attr(accessor().attr, utils.composeString(accessor()));
}
}
ko.bindingHandlers.fadeVisible = {
init: function(element, accessor) {
$(element)[accessor() ? "show" : "hide"]();
},
}
update: function(element, accessor) {
if (accessor() && $(element).is(":hidden")) {
var siblings = $(element).siblings(element.tagName + ":visible");
if (siblings.length) {
siblings.fadeOut("fast", function() {
$(element).fadeIn("fast");
})
} else {
$(element).fadeIn("fast");
}
}
}
});
Inthismodule,Isimplyaddmycustomdatabindingstotheko.bindingHandlersobject,andthere
arenonewfunctionstoexportdirectlyfromthemoduleforuseelsewhere.
249
www.it-ebooks.info
CHAPTER9WRITINGBETTERJAVASCRIPT
TipNoticethatthecustombindings-amdmoduledependsontheutils-amdmodule.TheAMDloaderis
responsibleforensuringthatallthedependenciesareresolved,whichmakesreusingmodulesverysimple.
Therequirecallbackfunctiondoesreceiveanargumentwhenamodulethatdoesn’treturnan
objectisloaded,butthevalueofthatargumentisnull.So,Icouldeasilyhavewrittenmycallback
functionlikethis:
require(libs, function(utils, device, bindings) {
...
}
Butthereislittlepointbecausethebindingsobjectwillbenull.Theorderoftheargumentsalways
reflectstheorderofthemodulesintherequirelist,soIalwaysputthemodulesthatdon’treturnobjects
attheendofthelistsothatIcanomitthenullargumentsthatcorrespondtothem.
DeclaringInlineDependencies
Itisn’talwayspossibletodeclareallofthedependenciesatthestartofascriptblock.Asanexample,in
ordertopreventjQueryMobilefromautomaticallyprocessingthedocument,IneedtoloadjQueryand
setupaneventhandlerbeforethejQueryMobilelibraryisloaded.Youcansimplycalltherequirejs
functiontodeclaredependencieswithinarequirestatement,likethis:
...
requirejs(['jquery.mobile-1.0.1.js'], function() {
$.mobile.initializePage();
$('a[data-role=button]').click(function(e) {
var count = 0;
utils.mapProducts(function(inner, outer) {
if (outer.category == e.currentTarget.id) {
count++;
}
}, cheeseModel.products, "items")
cheeseModel.selectedCount(count);
});
});
...
Inthisway,Iamabletodeclaremydependencieswithouthavingtoloadallofthecodefilesat
once.ThisgrantsmespacebetweenjQueryandjQueryMobilebeingloadedinwhichIcansetupmy
eventhandler.
ThisisalsothetechniqueIhaveusedinthedevice-amdmoduletoreplacetheModernizr.load
method.Listing9-14showsthecodefromChapter7whereIloadapolyfillbasedonthepresenceofa
browserfeature.
250
www.it-ebooks.info
CHAPTER9WRITINGBETTERJAVASCRIPT
Listing9-14.LoadingaPolyfillUsingModernizr
...
Modernizr.load([{
test: window.matchMedia,
nope: 'matchMedia.js',
complete: function() {
var screenQuery = window.matchMedia('screen AND (max-width: 500px)');
deviceConfig.smallScreen = ko.observable(screenQuery.matches);
if (screenQuery.addListener) {
screenQuery.addListener(function(mq) {
deviceConfig.smallScreen(mq.matches);
});
}
deviceConfig.largeScreen = ko.computed(function() {
return !deviceConfig.smallScreen();
});
setInterval(function() {
deviceConfig.smallScreen(window.innerWidth <= 500);
}, 500);
}
}, {
complete: function() {
callback(deviceConfig);
}
}]);
...
TheModernizrsyntaxisexcellent;Ilovebeingabletocombinethetest,loadingthedependency
andthecallbackfunctionsoelegantly.TherequireJSequivalentisshowninListing9-15,whichshows
thedevice-amd.jsfile.
Listing9-15.LoadingaPolyfillUsingrequireJS
define(['modernizr-2.0.6.js', 'knockout-2.0.0.js'], function() {
return {
detectDeviceFeatures: function(callback) {
var deviceConfig = {};
deviceConfig.landscape = ko.observable();
deviceConfig.portrait = ko.computed(function() {
return !deviceConfig.landscape();
});
var setOrientation = function() {
deviceConfig.landscape(window.innerWidth > window.innerHeight);
}
setOrientation();
251
www.it-ebooks.info
CHAPTER9WRITINGBETTERJAVASCRIPT
$(window).bind("orientationchange resize", function() {
setOrientation();
});
setInterval(setOrientation, 500);
if (window.matchMedia) {
var orientQuery = window.matchMedia('screen AND (orientation:landscape)')
if (orientQuery.addListener) {
orientQuery.addListener(setOrientation);
}
}
function setupMediaQuery() {
var screenQuery = window.matchMedia('screen AND (max-width: 500px)');
deviceConfig.smallScreen = ko.observable(screenQuery.matches);
if (screenQuery.addListener) {
screenQuery.addListener(function(mq) {
deviceConfig.smallScreen(mq.matches);
});
}
deviceConfig.largeScreen = ko.computed(function() {
return !deviceConfig.smallScreen();
});
setInterval(function() {
deviceConfig.smallScreen(window.innerWidth <= 500);
}, 500);
callback(deviceConfig);
}
if (window.matchMedia) {
setupMediaQuery();
} else {
requirejs(['matchMedia.js'], function() {
setupMediaQuery();
});
}
});
};
}
Thisisalesselegantapproach,butitdoesn’tsufferfromtheproblemsIdescribedearlierinthe
chapter.Ifyouareworkingonalargeprojectorsharingcodewithothers,thenasingle,coordinated
approachtodependencesisessential,evenifthecodestyleisn’tquiteassmooth.
252
www.it-ebooks.info
CHAPTER9WRITINGBETTERJAVASCRIPT
UnitTestingClient-SideCode
ThelasttopicthatIwanttocoverinthisbookisunittesting.Thetoolsforunittestingwebappsarenot
assophisticatedasthosefordesktoporserver-sidecode,buttheyarestillprettygood,andyouwillfind
iteasytoembraceclient-sideunittestingaspartofyourdevelopmentcycle—ifyouareabelieverinunit
testing,anyway.
AsIsaidatthebeginningofthischapter,Iamnotgoingtolectureyouabouttheimportanceof
testingortellyouwhenyoushouldbegintestingyourcode.Frommyownexperience,Iresistedunit
testingforalongtime,inpartbecauseofthenumberofzealotsthatkeptinsistingthattestingbedoneat
acertaintimeandinacertainway.Thesedays,Ihavecometoseethevalueinunittesting,butwhen
andhowunittestingisbestappliedvariesfromprojecttoprojectandprogrammertoprogrammer.Iam
abigbelieverinwritingbetter-qualitycode,butIhaveanintensedislikeforrigidapproachesthattreat
everysituationinthesameway.
Withthatinmind,Iamgoingtobrieflyintroduceyoutotheclient-sidetestingtoolthatIliketouse
andthenleaveyoutofigureouthowtoapplyit.Likeallofthetechniquesinthisbook,youshouldpick
whatworksforyou,adapteverythingtoyourownneeds,andsimplyignoreanythingthatdoesn’tsolve
anyproblemsyouarefacing.
UsingQUnit
IuseQUnit,whichisthetooldevelopedbythejQueryteamfortheirunittesting.Itissimpleand
effectiveandworkswell.YoucangetQUnitfromhttp://github.com/jquery/qunit.ToinstallQUnit,
downloadtheQUnitpackageandcopythequnit.jsandqunit.cssfilesfromthequnitfolderinthe
archivetotheNode.jscontentfolder.
QUnittestsarerunfromanHTMLdocument,andthereisabasicstructureofelementsrequiredin
thisdocumentsothatQUnitcandisplaythetestresults.Listing9-16showsthetemplatethatIused
whentestingAMDmodules,whichIhavecreatedasthefiletests.htmlinthecontentdirectory.
Listing9-16.AQUnitTemplateDocumentforAMDTesting
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="qunit.css"/>
<script src='require.js' type='text/javascript'></script>
<script src='jquery-1.7.1.js' type='text/javascript'></script>
<script src='qunit.js' type='text/javascript'></script>
<script type="text/javascript">
$(document).ready(function() {
require(["utils-amd"], function(utils) {
module("Utils-AMD Module");
// tests for utils-amd module will go here
});
});
</script>
</head>
<body>
<h1 id="qunit-header">AMD Tests</h1>
<h2 id="qunit-banner"></h2>
<div id="qunit-testrunner-toolbar"></div>
253
www.it-ebooks.info
CHAPTER9WRITINGBETTERJAVASCRIPT
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests"></ol>
<div id="qunit-fixture">test markup, will be hidden</div>
</body>
</html>
TouseQUnit,ensurethatthescriptandCSSfilesyoucopiedintothecontentdirectoryare
importedintothedocument.
ForeachmoduleIwanttotest,IusetheQUnitmodulefunctiontodenotethestartofaseriesoftests
anduserequireJStoloadthemodulecode.(TheQUnitmodulefunctionisn’trelatedtoAMDmodules;it
justgroupstogetherasetofrelatedtestsintheoutputdisplay.)
ThemarkupaddedtothetemplateallowsQUnittodisplaytheresults.Youcanchangethemarkup
toformatyourresultsdifferently,andinformationaboutthemeaningofeachelementcanbefoundat
http://docs.jquery.com/QUnit,alongwiththefullAPIdocumentation.
IhaveaddedjQuerytomylistofscriptimports,butQUnitdoesn’trequirejQuerytorun.Ifind
jQueryusefulforcreatingmorecomplextests,asI’lldemonstrateshortly.
TipBecarefulifyouareusingrequireJStoloadQUnit.TheQUnitlibraryinitializesitselfinresponsetotheload
eventonthewindowbrowserobject,andthiseventisusuallytriggeredbeforerequireJShasloadedthejQuery
libraryandexecutedthecallbackfunction.IfyouabsolutelymustuserequireJS,thenyoucanmakeacallto
QUnit.load()intherequireJScallbackfunction.
AddingTestsforaModule
Withthebasicstructureinplace,Icanbegintoaddtestsformymodule.Iamgoingtokeepthings
simpleandperformsomeargumenttestsonthecomposeStringfunction,makingsurenullarguments
don’tcauseoddresults.Listing9-17showstheadditionofteststothetests.htmlfile.
Listing9-17.AddingTeststothetests.htmlFile
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="qunit.css"/>
<script src='require.js' type='text/javascript'></script>
<script src='jquery-1.7.1.js' type='text/javascript'></script>
<script src='qunit.js' type='text/javascript'></script>
<script type="text/javascript">
$(document).ready(function() {
require(["utils-amd"], function(utils) {
module("Utils-AMD Module");
test("Null prefix and suffix", function() {
var config ={
prefix: null,
suffix: null,
value: "value"
254
www.it-ebooks.info
CHAPTER9WRITINGBETTERJAVASCRIPT
};
equal(utils.composeString(config), "value");
});
test("Null value", function() {
var config ={
prefix: "prefix",
suffix: "suffix",
value: null
};
equal(utils.composeString(config), "prefixsuffix");
});
test("No value property", function() {
var config ={
prefix: "prefix",
suffix: "suffix",
};
equal(utils.composeString(config), "prefixsuffix");
});
});
});
</script>
</head>
<body>
<h1 id="qunit-header">AMD Tests</h1>
<h2 id="qunit-banner"></h2>
<div id="qunit-testrunner-toolbar"></div>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests"></ol>
<div id="qunit-fixture">test markup, will be hidden</div>
</body>
</html>
Eachtestisdefinedwiththetestfunction,withargumentsforthenameofthetestandafunction
thatcontainsthetestcode.IneachofthefourtestsIhaveadded,Icreateanobjectwiththeprefix,
suffix,andvaluepropertiesthatarepassedtomyfunctionviamycustomdatabindingsandpassthis
tothecomposeStringfunction,whichIaccessthroughtheutilsargumenttomyrequireJScallback
function,likethis:
equal(utils.composeString(config), "prefixvalue");
Likemostunittestpackages,QUnitprovidesaseriesofassertionsthattesttheresultofan
operation.Inthiscase,Ihaveusedtheequalfunctiontocheckthattheresultfromcallingthe
composeStringfunctionmatchesmyexpectation.Arangeofdifferentassertionsareavailable,andyou
canseethefulllistathttp://docs.jquery.com/QUnit.
Toruntheunittests,simplyloadtests.htmlintothebrowser.QUnitwillperformeachtestinturn
andusethemarkupasacontainerfortheresults.MycomposeStringfunctionpassesoneofthetestsand
failstheothertwo.Theresultsaredisplayedinthebrowser,asshowninFigure9-2.
255
www.it-ebooks.info
CHAPTER9WRITINGBETTERJAVASCRIPT
Figure9-2.ExecutingunittestsonthecomposeStringfunction
ThereisabuginthecomposeStringfunction,whichdoesn’tchecktoseewhetherthevalueproperty
oftheobjectpassedastheargumentexistsorhasbeenassignedavalue.Tofixthisproblem,Imakethe
changeshowninListing9-18andrunthetestsagain.
Listing9-18.FixingthecomposeStringFunction
...
composeString: function(bindingConfig) {
var result = bindingConfig.value || "";
if (bindingConfig.prefix) { result = bindingConfig.prefix + result; }
if (bindingConfig.suffix) { result += bindingConfig.suffix;}
return result;
}
...
Icanrunindividualtestsagainor,byreloadingthedocument,runallofthetests.Mysimplefix
resolvestheproblemwiththetwobrokentests,andreloadingtests.htmlgivesmetheall-clear.
256
www.it-ebooks.info
CHAPTER9WRITINGBETTERJAVASCRIPT
UsingjQuerytoPerformTestsonHTML
IamnotgoingtowriteacompletesetoftestsformymodulesbecauseQUnitbehavesjustlikeanyother
unittestpackage,exceptitoperatesonJavaScriptinthebrowser,especiallyforself-containedfunctions
likecomposeStringwheretheinputandtheresultareallexpressedinJavaScript.
However,aslightlydifferentapproachisrequiredwhentheeffectorresultofthecodebeingtested
isexpressedinHTML.ThisisthereasonthatIincludedjQueryinmyQUnittesttemplate,andto
demonstratethistechnique,IwillwritesometestsfortheformatAttrbindinginthecustombindings-amd
module,whichisshowninListing9-19.
Listing9-19.TheformatAttrBindingfromthecustombindings-amdModule
ko.bindingHandlers.formatAttr = {
init: function(element, accessor) {
$(element).attr(accessor().attr, utils.composeString(accessor()));
},
update: function(element, accessor) {
$(element).attr(accessor().attr, utils.composeString(accessor()));
}
}
jQuerymakesiteasytocreate,use,andtestfragmentsofHTMLwithoutneedingtoaddthemtothe
document.Listing9-20showsadditionstotests.htmlfortheformatAttrbinding.
Listing9-20.UnitTestingUsingHTMLFragments
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="qunit.css"/>
<script src='require.js' type='text/javascript'></script>
<script src='jquery-1.7.1.js' type='text/javascript'></script>
<script src='qunit.js' type='text/javascript'></script>
<script type="text/javascript">
$(document).ready(function() {
require(["utils-amd"], function(utils) {
module("Utils-AMD Module");
// other utils-amd tests removed for brevity
test("No value property", function() {
var config ={
prefix: "prefix",
suffix: "suffix",
};
equal(utils.composeString(config), "prefixsuffix");
});
});
require(["custombindings-amd", "knockout-2.0.0.js"], function() {
module("Custombindings-AMD Module");
test("Correct attribute applied", function() {
var viewModel = {
257
www.it-ebooks.info
CHAPTER9WRITINGBETTERJAVASCRIPT
cat: "British"
};
var testElem = $("<a></a>").attr("data-bind",
"formatAttr: {attr: 'href', prefix: '#', value: cat}")[0];
ko.applyBindings(viewModel, testElem);
});
});
equal(testElem.attributes.length, 2);
equal($(testElem).attr("href"), "#British");
});
</script>
</head>
<body>
<h1 id="qunit-header">AMD Tests</h1>
<h2 id="qunit-banner"></h2>
<div id="qunit-testrunner-toolbar"></div>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests"></ol>
<div id="qunit-fixture">test markup, will be hidden</div>
</body>
</html>
IhaveaddedanewtestthatusesjQuerytocreateanaelementandapplyadata-bindattribute.If
youpassanHTMLfragmenttothejQuery$shorthandfunction,theresultisaDOMAPIelementthatis
notattachedtothedocument.Asabonus,Idon’thavetomakesurethatthesingleanddoublequotesin
thedata-bindattributeareproperlyescapedwhenusingthejQueryattrmethod:
var testElem = $("<a></a>").attr("data-bind",
"formatAttr: {attr: 'href', prefix: '#', value: cat}")[0];
NoticethatIusedanarray-styleindexertogetthefirstelementintheobjectreturnedbythejQuery
$shorthandfunction.Theko.applyBindingsmethodworksontheDOMAPIobjectratherthanjQuery
objectsandsoIneedtounwraptheaelementIhavecreatedfromthejQueryobject.Atthispoint,Ican
getKnockout.jstoapplybindingstomyHTMLfragmentusingmytestviewmodel:
ko.applyBindings(viewModel, testElem);
Totesttheresult,IusetheQUnitequalfunctionandboththeDOMAPIandjQuerytoinspectthe
result:
equal(testElem.attributes.length, 2);
equal($(testElem).attr("href"), "#British");
jQuerymakesiteasytocreateandprepareHTMLfortestingandchecktheresults,andasthis
exampleshows,youcanusetheDOMAPItogetinformationabouttheelementsafterthetesthas
completed.Asyoucansee,jQueryandQUnittogethermaketestingeveryaspectofawebapppossible
and,forthemostpart,easytodo.
258
www.it-ebooks.info
CHAPTER9WRITINGBETTERJAVASCRIPT
Summary
Inthischapter,IshowedyouthetoolsandtechniquesIusetowritebetterJavaScript,notbetterinthe
senseofamorecompleteuseofthelanguagefeaturesbutbetterinthesenseofeasierforotherstowork
with,easierformetomaintain,and,withtheapplicationofunittesting,sotheuserwillexperience
fewerproblems.Thesetechniques,combinedwiththosefromearlierchapters,giveyouasolid
foundationonwhichtobuildscalable,dynamic,andflexiblewebappsthatareeasytouseandeasyto
maintain.Goodluckonallofyourprojects,andremember,asIsaidinChapter1,thatanythingworth
doingontheserversideisworthconsideringfortheclientside,too.
259
www.it-ebooks.info
Index
A
Ajaxrequests,129
addingAjaxGETrequest,130–131
addingAjaxURLtomainmanifest,135
addingAjaxURLtomanifestNETWORK
section,135
errorhandling,133–134
POSTrequestbehavior,135
products.jsonFile,131
restructuring,131–133
Applicationcacheentries,122
Applicationcachespecification,126
Asynchronousmoduledefinition(AMD)
callbackarguments,248–250
definition,245
dependencydeclaration,247–248
dependencyissues,246–247
factoryfunction,245
inlinedependencies,250–252
D
DataStorage,browser.SeeHTML5localstorage
feature
Defaultactionsmanagement,27–29
Designpatterns,4
Desktopwebbrowser,7
Dynamicbasket,29
E
Emptybasket,71–74
Event,definition,24
F
Fallbackentries,122–126
Flowcontrolbindings,52–53
$function,18–19
B
G
Bidirectionalbindings,58–60
Graphicdesignandlayouts,4
C
H
Cache-controlheader,122
CheeseLux,9–12
addingrouting,101–105
browser,105
enhancingviewmodel,106
managingapplicationstate,107–108
mapProductsfunction,106
Clickevent,25–26
Code
fragment,6
HTMLdocument,5
Contentdistributionnetwork(CDN),16
CSSclass,22–24
Hovermethod,jQuery,26
HTMLeditor,7
HTML5HistoryAPI
preservingviewmodelstate,96–97
restoringapplicationstate,99–101
storingapplicationstate,98–99
HTML5localstoragefeature,137–139
complexdatastorage(seeIndexedDB)
withformelements,143–144
forJSONdata,139–141
withobjects,141–142
withofflinewebapplications
addingbuttons,154–155
261
www.it-ebooks.info
INDEX
HTML5localstoragefeature,withofflineweb
applications(cont.)
cachedCheeseLuxwebapp,150–153
createDialogfunction,156–157
enhanceViewModelfunction,153–154
scriptelementchanges,155–156
persistentforms,142–143
sessionstorage
benefits,148–149
semi-persistentobservabledataitem,149
synchronizingviewmodels,144
KOsubscribemethod,146
maindocumentmodification,147–148
persistentObservablefunction,144,146–
147
StorageEventobject,145
HTMLElementproperties,35
I
IndexedDB,156
DBOobject,158–160
locatingobjects
usingcursor,166
usingIndex,167
bykey,165–166
onupgradeneededproperty,160–161
successoutcomes,161
towebapplication,162–165
WebSQL,157
workingprinciple,157
namingcollision,229
property,methodandfunction,235–238
unittesting,253
addingtests,254–256
jQuery,257–258
QUnit,253–254
JavaScriptlibraries,7
JavaScriptpolyfilllibraries,129
jQuery
addClassmethod,23
bindmethod,changeandkeyupevents,32–
33
customselectors,20
hovermethod,26
importing,15–18
methodchaining,23
methodsforinsertingelementsindocument,
22
statement,19
UIbutton,43–44
UItoolkit,42–43
jQueryMobile
contentchanges,214–215
eventsequence,211–212
disablingautomaticprocessing,212–213
pageinitevent,213–214
pages,201–202
widgets,202
K
J
JavaScript
dependenciesinlibraries,238
AMDmodule(seeAsynchronousmodule
definition(AMD))
assumeddependency,238–239
directlyresolveddependency,240
double-loadingproblem,243–244
issues,240–243
globalnamespaces,229–230
globalvariables,230
namespaces
configuration,233–234
definition,230–231
nested,231–232
nested,usingafunction,232–233
self-executingfunction,234–235
Knockout(KO)
databindings,51
definition,49
library,49–53
koobject,50
L
Latentcontent,29,31–32
M
Methodchaining,23
Methodpairs,23
Mobilebrowseremulator,7
MobileWebApps,195
CheeseLuxMobileWebApp
basicimplementation,209
formatTextdatabinding,210–211
262
www.it-ebooks.info
INDEX
initialversion,206–209
duplicatingelementsusingtemplates,215
withcustomdata,218
data-bindattribute,218–220
usingtwo-passdatabindings,215–218
getIndexOfCategoryfunction,226
goals,205
jQueryMobile
askmobile.htmldocument,198
CheeseLuxwebapp,204
dataattributes,201
events,203
installation,201
setCookie,203
mobiledevicedetection
capabilities,197–198
useragent,195–196
multipagemodel
addingsupport,220–223
changePagemethod,225
mappingpagenamestoroutes,224–225
navigation,223
replacingradiobuttonswithanchors,223
Mouseenterandmouseleaveevents,26–27
N
Node.js,8
O
Offlinewebapps,109
Ajaxrequests
addingAjaxGETrequest,129–131
addingAjaxURLtomainmanifest,135
addingAjaxURLtomanifestNETWORK
section,135
errorhandling,133–134
POSTrequestbehavior,135
products.jsonfile,131
restructuring,131–133
HTML5applicationcache
acceptingchangestomanifest,115–116
addingmanifesttoHTMLdocument,
112–113
addingnetworkandfallbackentries,122–
126
cachedcontent,113–114
controlofcacheupdateprocess,116–122
manifestfile,111–112
monitoring
addingelementsandbindings,128
detectingstateofnetwork,126–128
POSTrequestbehavior,135
reviseddocument,109–111
P, Q
Polyfill,98
POSTrequestbehavior,135
R
RecurringAjaxrequestspolyfills,129
ResponsiveWebApps,169
screenorientation,184–188
screensize
adaptingsourcedata,183
adaptingwebapplayout,179–183
conditionaljQueryUIstyling,183–184
CSSmediaqueries,173–174
detectDeviceFeaturesfunction,177–178
imageloading,178–179
JavaScriptmediaqueries,174–175
matchMediafeature,176–177
polyfill,175–176
removingelements,184
touchinteraction
applicationroutes,193
detectingtouchsupport,189–190
navigation,191–193
touchSwipelibrary,190–191
viewport,169–172
S, T
Singlehandlerfunction,27
Submitbuttonupgradation
CSSclass,22–24
$function,18–19
inputelementselectionandhiding,19–21
jQuery,15–18
newelementinsertion,21–22
U
URLrouting
CheeseLux
addingrouting,101–104
263
www.it-ebooks.info
INDEX
URLrouting,CheeseLux(cont.)
browser,104
enhancingviewmodel,106
managingapplicationstate,107–108
mapProductsfunction,106
consolidatingroutes
addingdefaultroute,89–90
optionalsegments,88–89
unexpectedsegmentvalues,86–88
variablesegments,85–86
event-drivencontrolstonavigation
bridgingeventsandrouting,92–94
bridgingURLroutingandJavaScript
events,90–92
selecteddatabinding,94
usingHTML5HistoryAPI
history.replaceStatemethod,95
preservingviewmodelstate,96–97
restoringapplicationstate,99–101
stepsdemonstratingtheissue,95
storingapplicationstate,98–99
simpleroutedwebapplication,77–78
addingnavigationmarkup,81–83
addingroutinglibrary,79
addingviewmodelandcontentmarkup,
79–81
applyingcontrolsandelements,83–85
V
Valuebindings,51–52
Viewmodel,47
addingmoreproducts,53–54
dynamicbasket
addingbasketlineitems,66–69
addingbasketstructureandtemplate,70–
71
addingsubtotals,64–66
emptybasket,71–74
removingitems,71
generatingcontent,61–63
modelcreation
addingdatatodocument,48
adoptingviewmodellibrary,49
bidirectionalbindings,58–60
extendingthemodel.60–61
generatingcontent,49–53
observabledataitems,55–58
resetting,47–48
reviewing,63–64
URLrouting,79–80
W, X, Y, Z
Webappdevelopmentprinciples,15
dynamicbasketdata
addingbasketelements,29–31
bindmethod,changeandkeyupevents,
32–33
changingformtarget,37–39
latentcontent,31–32
overalltotalcalculation,35–37
subtotalcalculation,33–34
subtotaldisplay,34–35
eventhandling
clickevent,25–26
defaultactions,28–29
usingeventobject,27
mouseenterandmouseleave,26–27
singlehandlerfunction,27
JavaScriptdisabledandenabled,39–40
JavaScript-onlypolicy,41
non-JavaScriptusers,40
submitbuttonupgradation
CSSclass,22–24
$function,18–19
inputelementselectionandhiding,19–21
jQuery,15–19
newelementinsertion,21–22
UItoolkit
creatingjQueryUIbutton,43–44
settingupjQueryUI,42
Webserver,7
whitelistentries,122
WURFLdatabase,195
264
www.it-ebooks.info
ProJavaScriptforWeb
Apps
AdamFreeman
www.it-ebooks.info
ProJavaScriptforWebApps
Copyright©2012byAdamFreeman
Thisworkissubj ecttocopyright. Allrightsareres ervedbythePublisher,whetherthewholeorpartof thematerialis
concerned, specificallyth erightsof translati on,repr inting,reuseo fillus trations,recitation,broadcasti ng,
reproductiononmicrof ilmsori nany otherphysicalway ,an dt ransmissionori nformationstor agean dre trieval,
electronicadaptation,computersof tware,orbysi milarord issimilarmethodologynowknownorhereaf
ter
developed.Exemptedfromthislegalreservationarebriefexcerptsinconnectionwithreviewsorscholarlyanalysisor
materialsuppliedspecificallyforthepurposeofbeingenteredandexecutedonacomputersystem,forex clusiveuse
bythepurchaserofthework.Duplicationofthispublicationorpartsthereofispermittedonlyundertheprovisionsof
theCopyrightLawofthePublisher'slocation,ini tscurrentversion,andpermissionforusemustalwaysbeobtained
fromSpringer.PermissionsforusemaybeobtainedthroughRightsLinkattheCopyrightClearanceCenter.Violations
areliabletoprosecutionundertherespectiveCopyrightLaw.
ISBN-13(pbk):978-1-4302-4461-5
ISBN-13(electronic):978-1-4302-4462-2
Trademarkedn ames,logos,an d imagesmayapp earin this book.Ratherthanus eatrademarks ymbolwith every
occurrenceofatrademarkedname,logo,orima geweusethe names,logos,and imagesonlyina neditorialf ashion
andtothebenefitofthetrademarkowner,withnointentionofinfringementofthetrademark.
Theuseinthis publicationof tradenames,tr ademarks, servicema rks,a nds imilarterms,ev enifth eya re not
identifiedassuch,isnottobeta kenasanexpres sionofopinion astowhet herornottheyaresubjecttoproprietary
rights.
Whiletheadviceandinf ormationinthis bookarebelievedtobe trueandaccurateatthedateof publication,neither
theauthorsnor theeditorsnorth epublishercanacceptanylegal responsibilityforanyerrorsoro missionsthatmay
bemade.Thepublishermakesnowarranty,expressorimplied,withrespecttothematerialcontainedherein.
PresidentandPublisher:PaulManning
LeadEditor:BenRenow-Clarke
DevelopmentEditor:LouiseCorrigan
TechnicalReviewer:RJOwen
EditorialBoard: Ste veAn glin, EwanBuckin gham,Gary Corn ell,Louise Corrigan ,Morgan Erte l,Jon athan
Gennick,Jon athanHassell, RobertHutchin son, MichelleLowman ,Jame sMarkh am,Matthe wM oodie,Je ff
Olson,J effreyP epper,D ouglas Pundick,Ben R enow-Clarke,D ominicSha keshaft,G wenanSp earing,M att
Wade,TomWelsh
CoordinatingEditor:JenniferL.Blackwell
CopyEditor:KimWimpsett
Compositor:BythewayPublishingServices
Indexer:SPiGlobal
Artist:SPiGlobal
CoverDesigner:AnnaIshchenko
DistributedtothebooktradeworldwidebySpringerScie nce+BusinessMediaNewYork, 233SpringStreet,6thFloor,
New York,NY 10013.Phone 1-800-SPRINGER,f ax(20 1)348 -4505, e-mail orders-ny@springer-sbm.com,orv isit
www.springeronline.com.
Forinformationontranslations,pleasee-mailrights@apress.com,orvisitwww.apress.com.
Apressand friendsofEDbook smaybe purchasedinbulkf oracademic,corporate,orpromo tionaluse.eBoo k
versionsandlicensesarealsoavailableformostti tles.Formoreinformation,referenceourSpecialBulkSales–eBook
Licensingwebpageatwww.apress.com/bulk-sales.
Anysourcecodeorothersupplementary materialsref erencedbytheauthori n thiste xtisav ailabletore adersat
www.apress.com.Fordetailed
inf ormationabouthowtoloca
teyourbook’ssourcecode,
goto
www.apress.com/source-code.
ii
www.it-ebooks.info
Dedicatedtomylovelywife,JacquiGriffyth.
iii
www.it-ebooks.info
Contents
AbouttheAuthor................................................................................................... xii
AbouttheTechnicalReviewer ............................................................................. xiii
Acknowledgments ............................................................................................... xiv
Chapter1:GettingReady ........................................................................................1
AboutThisBook.................................................................................................................1
WhoAreYou? ........................................................................................................................................... 1
WhatDoYouNeedtoKnowBeforeYouReadThisBook?........................................................................ 2
WhatIfYouDon’tHaveThatExperience? ................................................................................................ 2
IsThisaBookAboutHTML5?................................................................................................................... 2
WhatIstheStructureofThisBook? ......................................................................................................... 2
DoYouDescribeDesignPatterns? ........................................................................................................... 4
DoYouTalkAboutGraphicDesignandLayouts?..................................................................................... 4
WhatIfYouDon’tLiketheTechniquesorToolsIDescribe? .................................................................... 5
IsThereaLotofCodeinThisBook? ........................................................................................................ 5
WhatSoftwareDoYouNeedforThisBook?......................................................................6
GettingtheSourceCode........................................................................................................................... 6
GettinganHTMLEditor............................................................................................................................. 7
GettingaDesktopWebBrowser............................................................................................................... 7
GettingaMobileBrowserEmulator.......................................................................................................... 7
GettingtheJavaScriptLibraries ............................................................................................................... 7
GettingaWebServer................................................................................................................................ 7
IntroducingtheCheeseLuxExample .................................................................................9
v
www.it-ebooks.info
CONTENTS
FontAttribution................................................................................................................12
Summary .........................................................................................................................13
Chapter2:GettingStarted ....................................................................................15
UpgradingtheSubmitButton ..........................................................................................15
PreparingtoUsejQuery.......................................................................................................................... 15
UnderstandingtheReadyEvent ............................................................................................................. 18
SelectingandHidingtheInputElement ................................................................................................. 19
InsertingtheNewElement ..................................................................................................................... 21
ApplyingaCSSClass.............................................................................................................................. 22
RespondingtoEvents ......................................................................................................24
HandlingtheClickEvent......................................................................................................................... 25
HandlingMouseHoverEvents................................................................................................................ 26
UsingtheEventObject ........................................................................................................................... 27
DealingwithDefaultActions .................................................................................................................. 28
AddingDynamicBasketData ..........................................................................................29
AddingtheBasketElements................................................................................................................... 29
ShowingtheLatentContent ................................................................................................................... 31
RespondingtoUserInput ....................................................................................................................... 32
CalculatingtheOverallTotal................................................................................................................... 35
ChangingtheFormTarget...................................................................................................................... 37
UnderstandingProgressiveEnhancement.......................................................................39
RevisitingtheButton:UsingaUIToolkit..........................................................................42
SettingUpjQueryUI................................................................................................................................ 42
CreatingajQueryUIButton .................................................................................................................... 43
Summary .........................................................................................................................45
vi
www.it-ebooks.info
CONTENTS
Chapter3:AddingaViewModel...........................................................................47
ResettingtheExample.....................................................................................................47
CreatingaViewModel.....................................................................................................48
AdoptingaViewModelLibrary............................................................................................................... 49
GeneratingContentfromtheViewModel............................................................................................... 49
TakingAdvantageoftheViewModel ..............................................................................53
AddingMoreProductstotheViewModel .............................................................................................. 53
CreatingObservableDataItems ............................................................................................................. 55
CreatingBidirectionalBindings .............................................................................................................. 58
AddingaDynamicBasket................................................................................................64
AddingSubtotals .................................................................................................................................... 64
AddingtheBasketLineItemsandTotal ................................................................................................. 66
FinishingtheExample ............................................................................................................................ 71
Summary .........................................................................................................................74
Chapter4:UsingURLRouting ...............................................................................77
BuildingaSimpleRoutedWebApplication......................................................................77
AddingtheRoutingLibrary ..................................................................................................................... 78
AddingtheViewModelandContentMarkup ......................................................................................... 79
AddingtheNavigationMarkup ............................................................................................................... 81
ApplyingURLRouting ............................................................................................................................. 83
ConsolidatingRoutes .......................................................................................................85
UsingVariableSegments........................................................................................................................ 85
UsingOptionalSegments ....................................................................................................................... 88
AddingaDefaultRoute........................................................................................................................... 88
AdaptingEvent-DrivenControlstoNavigation.................................................................89
UsingtheHTML5HistoryAPI ...........................................................................................94
AddingHistoryStatetotheExampleApplication ................................................................................... 95
vii
www.it-ebooks.info
CONTENTS
AddingURLRoutingtotheCheeseLuxWebApp ...........................................................101
MovingthemapProductsFunction....................................................................................................... 105
EnhancingtheViewModel ................................................................................................................... 105
ManagingApplicationState.................................................................................................................. 106
Summary .......................................................................................................................108
Chapter5:CreatingOfflineWebApps.................................................................109
ResettingtheExample...................................................................................................109
UsingtheHTML5ApplicationCache..............................................................................111
UnderstandingWhenCachedContentIsUsed ..................................................................................... 113
AcceptingChangestotheManifest...................................................................................................... 115
TakingControloftheCacheUpdateProcess ....................................................................................... 116
AddingNetworkandFallbackEntriestotheManifest ......................................................................... 122
MonitoringOfflineStatus...............................................................................................126
UnderstandingwithAjaxandPOSTRequests ...............................................................129
UnderstandingtheDefaultAjaxGETBehavior...................................................................................... 131
AddingtheAjaxURLtotheMainManifestorFALLBACKSections....................................................... 135
AddingtheAjaxURLtotheManifestNETWORKSection ...................................................................... 135
UnderstandingthePOSTRequestBehavior.......................................................................................... 135
Summary .......................................................................................................................136
Chapter6:StoringDataintheBrowser ..............................................................137
UsingLocalStorage.......................................................................................................137
StoringJSONData ................................................................................................................................ 139
StoringFormData ................................................................................................................................ 142
SynchronizingViewModelDataBetweenDocuments ..................................................144
UsingSessionStorage...................................................................................................148
UsingLocalStoragewithOfflineWebApplications.......................................................149
UsingLocalStoragewithOfflineForms ............................................................................................... 153
viii
www.it-ebooks.info
CONTENTS
UsingPersistenceintheOfflineApplication......................................................................................... 154
StoringComplexData ....................................................................................................156
CreatingtheIndexedDBDatabaseandObjectStore ............................................................................ 158
IncorporatingtheDatabaseintotheWebApplication .......................................................................... 162
LocatinganObjectbyKey .................................................................................................................... 165
LocatingObjectsUsingaCursor........................................................................................................... 166
LocatingObjectsUsinganIndex .......................................................................................................... 167
Summary .......................................................................................................................167
Chapter7:CreatingResponsiveWebApps.........................................................169
SettingtheViewport ......................................................................................................169
RespondingtoScreenSize............................................................................................173
UsingMediaQuerieswithJavaScript................................................................................................... 174
AdaptingtheWebAppLayout .............................................................................................................. 179
RespondingtoScreenOrientation .................................................................................184
IntegratingScreenOrientationintotheWebApp ................................................................................. 186
RespondingtoTouch .....................................................................................................188
DetectingTouchSupport ...................................................................................................................... 189
UsingTouchtoNavigatetheWebAppHistory ..................................................................................... 191
IntegratingwiththeApplicationRoutes ............................................................................................... 193
Summary .......................................................................................................................194
Chapter8:CreatingMobileWebApps ................................................................195
DetectingMobileDevices ..............................................................................................195
DetectingtheUserAgent...................................................................................................................... 195
DetectingDeviceCapabilities ............................................................................................................... 196
CreatingaSimpleMobileWebApp ...............................................................................198
InstallingjQueryMobile ........................................................................................................................ 201
UnderstandingthejQueryMobileDataAttributes ................................................................................ 201
ix
www.it-ebooks.info
CONTENTS
DealingwithjQueryMobileEvents. ..................................................................................................... 203
StoringtheUser’sDecision . ................................................................................................................ 203
DetectingtheUser’sDecisionintheWebApp . ................................................................................... 204
BuildingtheMobileWebApp.........................................................................................205
ManagingtheEventSequence . ........................................................................................................... 211
PreparingforContentChanges . .......................................................................................................... 214
DuplicatingElementsandUsingTemplates ..................................................................215
UsingTwo-PassDataBindings. ........................................................................................................... 215
AdoptingtheMultipageModel.......................................................................................220
ReworkingCategoryNavigation . ......................................................................................................... 223
ReplacingRadioButtonswithAnchors . .............................................................................................. 223
MappingPageNamestoRoutes . ........................................................................................................ 224
ExplicitlyChangingPages . .................................................................................................................. 225
AddingtheFinalChrome ...............................................................................................225
Summary .......................................................................................................................228
Chapter9:WritingBetterJavaScript..................................................................229
ManagingtheGlobalNamespace ..................................................................................229
DefiningaJavaScriptNamespace. ...................................................................................................... 230
UsingSelf-executingFunctions. .......................................................................................................... 234
CreatingPrivateProperties,Methods,andFunctions ...................................................235
ManagingDependencies ...............................................................................................238
UnderstandingAssumedDependencyProblems. ................................................................................ 238
UnderstandingDirectlyResolvedDependencies . ................................................................................ 240
MakingaBadProblemintoaSubtleBadProblem. ............................................................................. 243
UsingtheAsynchronousModuleDefinition. ........................................................................................ 244
UnitTestingClient-SideCode ........................................................................................253
UsingQUnit. ......................................................................................................................................... 253
x
www.it-ebooks.info
CONTENTS
AddingTestsforaModule.................................................................................................................... 254
UsingjQuerytoPerformTestsonHTML............................................................................................... 257
Summary .......................................................................................................................259
Index ...................................................................................................................261
xi
www.it-ebooks.info
About the Author
AdamFreemanisanexperiencedITprofessionalwhohasheldsenior
positionsinarangeofcompanies,mostrecentlyservingaschieftechnology
officerandchiefoperatingofficerofaglobalbank.Nowretired,hespendshis
timewritingandrunning.
xii
www.it-ebooks.info
About the Technical Reviewer
RJOwenistheleadexperienceplanneratEffectiveUI,focusingon
customerinsightwork,includingethnographicresearch,designvalidation,
co-creationexercises,andexpertdesign.RJstartedhiscareerasasoftware
developerandspenttenyearsworkinginC++,Java,andFlexbeforemoving
tothedesignresearchandcustomerinsightteamatEffectiveUI.Hetruly
lovesgooddesignandunderstandingwhatmakespeopletick.RJholdsan
MBAandabachelor’sinphysicsandcomputerscience.Heisafrequent
speakeratmanyindustryevents,includingWeb2.0,SXSW,andAdobe
MAX.
xiii
www.it-ebooks.info
Acknowledgments
IwouldliketothankeveryoneatApressforworkingsohardtobringthisbooktoprint.Inparticular,
IwouldliketothankJenniferBlackwellforkeepingmeontrackandBenRenow-Clarkefor
commissioningandeditingthistitle.Iwouldalsoliketothankmytechnicalreviewer,RJOwen,
whoseeffortsmadethisbookfarbetterthanitwouldhavebeenotherwise.
xiv
www.it-ebooks.info