freeman fm-1 final · client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ......

276
www.allitebooks.com

Upload: others

Post on 15-Aug-2020

1 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

www.allitebooks.com

Page 2: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

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.allitebooks.com

Page 3: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

iv

Contents at a Glance

AbouttheAuthor................................................................................................... xiiAbouttheTechnicalReviewer ............................................................................. xiiiAcknowledgments ............................................................................................... xivChapter1:GettingReady ........................................................................................1Chapter2:GettingStarted ....................................................................................15Chapter3:AddingaViewModel...........................................................................47Chapter4:UsingURLRouting...............................................................................77Chapter5:CreatingOfflineWebApps.................................................................109Chapter6:StoringDataintheBrowser ..............................................................137Chapter7:CreatingResponsiveWebApps.........................................................169Chapter8:CreatingMobileWebApps ................................................................195Chapter9:WritingBetterJavaScript..................................................................229Index ...................................................................................................................261

www.allitebooks.com

Page 4: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

C H A P T E R 1

1

Getting Ready

Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver-sidecoding.Thisstartedbecausebrowsersandthedevicestheyrunonhavebeenlesscapablethanenterprise-classservers.Toprovideanykindofseriouswebappfunctionality,theserverhadtodoalloftheheavyliftingforthebrowsers,whichwasprettydumbandsimplebycomparison.

Overthelastfewyears,browsershavegotsmarter,morecapable,andmoreconsistentinhowtheyimplementwebtechnologyandstandards.Whatusedtobeafighttocreateuniquefeatureshasbecomeabattletocreatethefastestandmostcompliantbrowser.Theproliferationofsmartphonesandtabletshascreatedahugemarketforhigh-qualitywebapps,andthegradualadoptionofHTML5provideswebapplicationdeveloperswithasolidfoundationforbuildingrichandfluidclient-sideexperiences.

Sadly,whiletheclient-sidetechnologyhascaughtupwiththeserverside,thetechniquesthatclient-sideprogrammersusestilllagbehind.Thecomplexityofclient-sidewebappshasreachedatippingpointwherescale,elegance,andmaintainabilityareessentialandthedaysofhackingoutaquicksolutionhavepassed.Inthisbook,Ileveltheplayingfield,showingyouhowtostepupyourclient-sidedevelopmenttoembracethebesttechniquesfromtheserver-sideworldandcombinethemwiththelatestHTML5features.

AboutThisBookThisismy15thbookabouttechnology,andtomarkthis,Apressaskedmetodosomethingdifferent:sharethetools,tricks,andtechniquesthatIusetocreatecomplexclient-sidewebapps.Theresultissomethingthatismorepersonal,informal,andeclecticthanmyregularwork.Ishowyouhowtotakeindustrial-strengthdevelopmentconceptsfromserver-sidedevelopmentandapplythemtothebrowser.Byusingthesetechniques,youcanbuildwebappsthatareeasiertowrite,areeasiertomaintain,andofferbetterandricherfunctionalitytoyourusers.

WhoAreYou?Youareanexperiencedwebdeveloperwhoseprojectshavestartedtogetoutofcontrol.ThenumberofbugsinyourJavaScriptcodeisincreasing,andittakeslongertofindandfixeachone.Youaretargetinganever-widerrangeofdevice,includingdesktops,tablets,andsmartphones,andkeepingitallworkingisgettingtougher.Yourworkingdaysarelonger,butyouhavelesstimetospendonnewfeaturesbecausemaintainingthecodeyoualreadyhavesucksupabigchuckofyourtime.

Theexcitementthatcomesfromyourworkhasfaded,andyouhaveforgottenwhatitfeelsliketohaveareallyproductivedayofcoding.Youknowsomethingiswrong,youknowthatyouarelosingyourgrip,andyouknowyouneedtofindadifferentapproach.Ifthissoundsfamiliar,thenyouaremytargetreader.

www.allitebooks.com

Page 5: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER1GETTINGREADY

2

WhatDoYouNeedtoKnowBeforeYouReadThisBook?Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammertounderstandthecontent.YouneedaworkingknowledgeofHTML,youneedtoknowhowtowriteJavaScript,andyouhaveusedbothtocreateclient-sidewebapps.Youwillneedtounderstandhowabrowserworks,howHTTPfitsintothepicture,andwhatAjaxrequestsareandwhyyoushouldcareaboutthem.

WhatIfYouDon’tHaveThatExperience?Youmaystillgetsomebenefitfromthisbook,butyouwillhavetofigureoutsomeofthebasicsonyourown.Ihavewrittenacoupleofotherbooksyoumightfindusefulasprimersforthisone.IfyouarenewtoHTML,thenreadTheDefinitiveGuidetoHTML5.Thisexplainseverythingyouneedtocreateregularwebcontentandbasicwebapps.IexplainhowtouseHTMLmarkupandCSS3(includingthenewHTML5elements)andhowtousetheDOMAPIandtheHTML5APIs(includingaJavaScriptprimerifyouarenewtothelanguage).ImakealotofuseofjQueryinthisbook.Iprovidealloftheinformationyouneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksandhowitrelatestotheDOMAPI,thenreadProjQuery.BothofthesebooksarepublishedbyApress.

Booksaside,youcanlearnalotaboutHTMLandthebrowserAPIsbyreadingthespecificationspublishedbytheW3Catwww.w3.org.Thespecificationsareauthoritativebutcanbehard-goingandarenotalwaysthatclear.AmorereadilyaccessibleresourceistheMozillaDeveloperNetworkathttp://developer.mozilla.org.ThisisanexcellentsourceofinformationabouteverythingfromHTMLtoJavaScript.ThereisageneralbiastowardFirefox,butthisisn’tusuallyaproblemsincethemainstreambrowsersaregenerallycompliantandconsistentinthewaytheyimplementwebstandards.

IsThisaBookAboutHTML5?No,althoughIdotalkaboutsomeofthenewHTML5JavaScriptAPIs.Mostofthisbookisabouttechnique,mostofwhichwillworkwithHTML4justasitdoeswithHTML5.SomechaptersarebuiltpurelyonHTML5APIs(suchasChapters5and6,whichshowyouhowtocreatewebappsthatworkofflineandhowtostoredatainthebrowser),buttheotherchaptersarenottiedtoanyparticularversionofHTML.Idon’tgetintoanydetailaboutthenewelementsdescribedinHTML5.Thisisabookaboutprogramming,andthenewelementsdon’thavemuchimpactonJavaScriptprogramming.

WhatIstheStructureofThisBook?InChapter2,IbuildasimplewebappforafictitiouscheeseretailercalledCheeseLux,buildingonthebasicexampleIintroducelaterinthischapter.Ifollowsomeprettystandardapproachesforcreatingthiswebappandspendtherestofthebookshowingyouhowtoapplyindustrial-strengthtechniquestoimprovedifferentaspects.Ihavetriedtokeepeachchapterreasonablyseparate,butthisisareasonablyinformalbook,andIdointroducesomeconceptsgraduallyoveranumberofchapters.Eachchapterbuildsonthetechniquesintroducedinthechaptersthatgobeforeit.Youshouldreadthebookinchapterorderifyoucan.Thefollowingsectionssummarizethechaptersinthisbook.

Chapter1:GettingReadyAsidefromdescribingthisbook,IintroducethestaticHTMLversionoftheCheeseLuxexample,whichIusethroughoutthisbook.Ialsolistthesoftwareyouwillneedifyouwanttore-createtheexamplesonyourownorexperimentwiththelistingsthatareincludedinthesourcecodedownloadthataccompaniesthisbook(andwhichisavailablefreefromApress.com).

www.allitebooks.com

Page 6: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER1GETTINGREADY

3

Chapter2:GettingStartedInthischapter,IusesomebasictechniquestocreateamoredynamicversionoftheCheeseLuxexample,movingfromawebsitetoawebapp.IusethisasanopportunitytointroducesomeofthetoolsandconceptsthatyouwillneedfortherestofthebookandtoprovideacontextsothatIcanshowbettertechniquesinlaterchapters.

Chapter3:AddingaViewModelThefirstadvancedtechniqueIdescribeisintroducingaclient-sideviewmodelintoawebapp.ViewmodelsareakeycomponentindesignpatternssuchasModelViewController(MVC)andModel-View-ViewModel.Ifyouadoptonlyonetechniquefromthisbook,thenmakeitthisone;itwillhavethebiggestimpactonyourdevelopmentpractices.

Chapter4:UsingURLRoutingURLroutingallowsyoutoscaleupthenavigationmechanismsinyourwebapps.Youmaynothaverealizedthatyouhaveanavigationproblem,butwhenyouseehowURLroutingcanworkontheclientside,youwillseejusthowpowerfulandflexibleatechniqueitcanbe.

Chapter5:CreatingOfflineWebAppsInthischapter,IshowyouhowtousesomeofthenewHTML5JavaScriptAPIstocreatewebappsthatworkevenwhentheuserisoffline.Thisisapowerfultechniquethatisincreasinglyimportantassmartphonesandtabletsgainmarketpenetration.Theideaofanalways-onnetworkconnectionischanging,andbeingabletoaccommodateofflineworkingisessentialformanywebapps.

Chapter6:StoringDataBeingabletorunthewebappofflineisn’tmuchuseunlessyoucanalsoaccessstoreddata.Inthischapter,IshowyouthedifferentHTML5APIsthatareavailableforstoringdifferentkindsofdata,rangingfromsimplename/valuepairstosearchablehierarchiesofpersistedJavaScriptobjects.

Chapter7:CreatingResponsiveWebAppsThereareentirecategoriesofweb-enableddevicesthatfalloutsideofthetraditionaldesktopandmobiletaxonomy.Oneapproachtodealingwiththeproliferationofdifferentdevicetypesistocreatewebappsthatadaptdynamicallytothecapabilitiesofthedevicetheyarebeingusedon,tailoringtheirappearance,functionality,andinteractionmodelsasrequired.Inthischapter,Ishowyouhowtodetectthecapabilitiesyoucareaboutandrespondtothem.

www.allitebooks.com

Page 7: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER1GETTINGREADY

4

Chapter8:CreatingMobileWebAppsAnalternativetocreatingresponsivewebappsistocreateaseparateversionthattargetsaspecificrangeofdevices.Inthischapter,IshowyouhowtousejQueryMobiletocreatesuchawebappandhowtoincorporateadvancedfeaturessuchasURLroutingintoamobilewebapp.

Chapter9:WritingBetterJavaScriptThelastchapterinthisbookisaboutimprovingyourcode—notintermsofusingJavaScriptbetterbutintermsofcreatingeasilymaintainedcodemodulesthatareeasiertouseinyourownprojectsandeasiertosharewithothers.Ishowyousomeconvention-basedapproachesandintroducetheAsynchronousModuleDefinition,whichsolvessomecomplexproblemswhenexternallibrarieshavedependenciesonotherfunctionality.Ialsoshowyouhowyoucaneasilyapplyunittestingtoyourclient-sidecode,includinghowtounittestcomplexHTMLtransformations.

DoYouDescribeDesignPatterns?Idon’t.Thisisn’tthatkindofbook.Thisisabookaboutgettingresults,andIdon’tspendalotoftimediscussingthedesignpatternsthatunderpineachtechniqueIdescribe.Ifyouarereadingthisbook,thenyouwanttoseethoseresultsandgetthebenefitstheyprovidenow.Myadviceistosolveyourimmediateproblemsandthenstartresearchingthetheory.Alotofgoodinformationisavailableaboutdesignpatternsandtheassociatedtheory.Wikipediaisagoodplacetostart.SomereadersmaybesurprisedattheideaofWikipediaasasourceofprogramminginformation,butitoffersawealthofwell-balancedandwell-writtencontent.

Ilovedesignpatterns.Ithinktheyareimportantandusefulandavaluablemechanismforcommunicatinggeneralsolutionstocomplexproblems.Sadly,theyarealltoooftenusedasakindofreligion,whereeveryaspectofapatternmustbeappliedexactlyasspecifiedandlongandnastyconflictsbreakoutaboutthemeritsandapplicabilityofcompetingpatterns.

Myadviceistoconsiderdesignpatternsasthefoundationfordevelopingtechniques.Mixandmatchdifferentdesignpatternstosuityourprojectsandcherry-pickthebitsthatsolvetheproblemsyouface.Don’tletanyonedictatethewaythatyouusepatterns,andalwaysremainfocusedonfixingrealproblemsinrealprojectsforrealusers.Thedayyoustartarguingaboutsolutionstotheoreticalproblemsisthedayyougoovertothedarkside.Bestrong.Stayfocused.Resistthepatternzealots.

DoYouTalkAboutGraphicDesignandLayouts?No.Thisisn’tthatkindofbook,either.Thelayoutoftheexamplewebappsisprettysimple.Thereareacoupleofreasonsforthis.Thefirstisthatthisisabookaboutprogramming,andwhileIspendalotoftimeshowingyoutechniquesformanagingmarkupdynamically,theactualvisualeffectisverymuchasideeffect.

ThesecondreasonisthatIhavetheartisticabilitiesofalemon.Idon’tdraw,Idon’tpaint,andIdon’thaveasidelinebusinesssellingmyoil-on-canvasworkatalocalgallery.Infact,asachildIwasexcusedfromartlessonsbecauseofatotalandabsolutelackoftalent.Iamaprettygoodprogrammer,butmydesignskillssuck.Inthisbook,IsticktowhatIknow,whichisheavy-dutyprogramming.

www.allitebooks.com

Page 8: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER1GETTINGREADY

5

WhatIfYouDon’tLiketheTechniquesorToolsIDescribe?Thenyouadaptthetechniquesuntilyoudolikethemandfindalternativetoolsthatworkthewayyouprefer.Thecriticalinformationinthisbookisthatyoucanapplyheavy-dutyserver-sidetechniquestocreatebetterwebapps.Thefineimplementationdetailisn’timportant.Mypreferredtoolsandtechniquesworkwellforme,andifyouthinkaboutcodeinthewayIdo,theywillworkwellforyoutoo.Butifyourmindworksinadifferentway,changethebitsofmyapproachthatdon’tfit,discardthebitsthatdon’twork,andusewhat’sleftasafoundationforyourownapproaches.We’llbothcomeoutaheadaslongasyouendupwithwebappsthatscalebetter,makeyourcodingmoreenjoyable,andreducetheburdenofmaintenance.

IsThereaLotofCodeinThisBook?Yes.Infact,thereissomuchcodethatIcouldn’tfititallin.Bookshaveapagebudget,whichissetrightatthestartoftheproject.Thepagebudgetaffectsthescheduleforthebook,theproductioncost,andthefinalpricethatthebooksellsfor.Stickingtothepagebudgetisabigdeal,andmyeditorgetsuncomfortablewheneverhethinksIamgoingtorunlong(hi,Ben!).IhadtodosomeeditingtofitinallofthecodeIwantedtoinclude.So,whenIintroduceanewtopicormakealotofchangesinonego,I’llshowyouacompleteHTMLdocumentorJavaScriptcodefile,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>

www.allitebooks.com

Page 9: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER1GETTINGREADY

6

</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.Thefulllistinggivesyouawidercontextabouthowthetechniqueathandfitsintothewebappworld.WhenIamshowingasmallchangeoremphasizingaparticularregionofcode,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-2showsametaelementbeingaddedtotheheadsectionofListing1-1.Youdon’thavetoapplythesechangesyourselfifyouwanttoexperimentwiththeexamples.Instead,youcandownloadacompletesetofeverycodelistinginthisbookfromApress.com.Thisfreedownloadalsoincludestheserver-sidecodethatIrefertolaterinthischapterandusethroughoutthisbooktocreatedifferentaspectsofthewebapp.

WhatSoftwareDoYouNeedforThisBook?Youwillneedafewpiecesofsoftwareifyouwanttore-createtheexamplesinthisbook.Therearelotsofchoicesforeachtype,andtheonesthatIuseareallavailablewithoutcharge.Idescribeeachinthesectionsthatfollowalongwithmypreferredtoolineachcategory.

GettingtheSourceCodeYouwillneedtodownloadthesourcecodethataccompaniesthisbook,whichisavailablewithoutchargefromApress.com.Thesourcecodedownloadcontainsallofthelistingsorganizedbychapterandallofthesupportingresources,suchasimagesandstylesheets.Youwillneedthecontentsofthisdownloadifyouwanttocompletelyre-createanyoftheexamples.

www.allitebooks.com

Page 10: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER1GETTINGREADY

7

GettinganHTMLEditorAlmostanyeditorcanbeusedtoworkwithHTML.Idon’trelyonanyspecialfeaturesinthisbook,sousewhatevereditorsuitsyou.IuseKomodoEditfromActiveState.ItisfreeandsimpleandhasprettygoodsupportforHTML,JavaScript,jQuery,andNode.js.IhavenoaffiliationwithActiveStateotherthanasahappyuser.YoucangetKomodoEditfromhttp://activestate.com,andthereareversionsforWindows,Mac,andLinux.

GettingaDesktopWebBrowserAnymodernmainstreamdesktopbrowserwillruntheexamplesinthisbook.IlikeGoogleChrome;Ifinditquick,IlikethesimpleUI,andthedevelopertoolsareprettygood.MostofthescreenshotsinthisbookareofGoogleChrome,althoughtherearetimeswhenIuseFirefoxbecauseChromedoesn’timplementanHTML5featurefully.(ThesupportforHTML5APIsisabitmixedasIwritethis,buteverybrowserreleaseimprovesthesituation.)

GettingaMobileBrowserEmulatorInChapters7and8,Italkabouttargetingdifferentkindsofdevices.Itcanbeslowandfrustratingworkdealingwithrealdevicesduringtheearlystagesofdevelopment,soIuseamobilebrowseremulatortogetstartedandputthemajorfunctionalitytogether.Itisn’tuntilIhavesomethingfunctionalandsolidthatIstarttestingonrealmobiledevices.

IliketheOperaMobileemulator,whichyoucangetforfreefromwww.opera.com/developer/tools/mobile;thereareversionsavailableforWindows,Mac,andLinux.Theemulatorusesthesamecodebaseastherealand,widelyused,OperaMobile,andwhiletherearesomequirks,theexperienceisprettyfaithfultotheoriginal.Ilikethispackagebecauseitletsmecreateemulatorsfordifferentscreensizesfromsmall-screenedsmartphonesrightthroughtoHDtablets.Thereissupportforemulatingtoucheventsandchangingtheorientationofthedevice.YoucanruntheexamplesinChapters7and8inanybrowser,butpartofthepointofthesechaptersistoelegantlydetectmobiledevices,andyou’llgetthebestresultsbyusinganemulator,evenifitisn’ttheoneforOpera.

GettingtheJavaScriptLibrariesIdon’tbelieveinre-creatingfunctionalitythatisavailableinawell-written,publicallyavailableJavaScriptlibrary.Tothatend,thereareanumberoflibrariesthatIuseineachchapter.Some,suchasjQuery,jQueryUI,andjQueryMobile,arewell-known,buttherearealsosomethatprovidesomenichefeaturesorcoveragapinbrowsersthatdon’timplementcertainHTML5APIs.ItellyouhowtoobtaineachlibraryasIintroduceit,andtheycanallbefoundinthesourcecodedownloadthatisavailablefromApress.com.Youdon’tneedtousethelibrariesthatIlikeinordertousethetechniquesIdiscuss,butyouwillneedthemtore-createtheexamples.

GettingaWebServerTheexamplesinthisbookarefocusedontheclient-sidewebapps,butsometechniquesrequirecertainbehaviorsfromtheserver.Mostoftheexampleswillworkwithcontentservedupbyanywebserver,butyouwillneedtouseNode.jsifyouwanttore-createeveryexampleinthisbook.

ThereasonthatIchoseNode.jsisthatitiswritteninJavaScriptandissupportedonawiderangeofplatforms.Thismeansthatanyreaderofthisbookwillbeabletosetuptheserverandreadandunderstandthecodethatdrivestheserver.

www.allitebooks.com

Page 11: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER1GETTINGREADY

8

Theserver-sidecodeisincludedinthesourcecodedownloadfromApress.com,inafilecalledserver.js.Iamnotgoingtogointoanydetailaboutthiscode,andIamnotevengoingtolistit.Itdoesn’tdoanythingspecial;itjustservesupcontentandhasafewspecialURLsthatallowmetopostdatafromtheexamplewebappandgetatailoredresponse.TherearesomeotherURLsthatcreateparticulareffects,suchasaddingadelaytosomerequests.Takealookatserver.jsifyouwanttoseewhat’sthere,butyoudon’tneedtounderstand(orevenlookat)theserver-sidecodetogetthebestfromthisbook.

Youwill,however,needtoinstallandsetupNode.jssothatitisrunningonyournetwork.Iprovideinstructionsforgettingupandrunninginthesectionsthatfollow.

GettingandPreparingNode.jsYoucandownloadNode.jsfromhttp://nodejs.org.InstallationpackagesareavailableforWindows,Mac,andLinux,andthesourcecodeisavailableifyouwanttocompileforadifferentplatform.TheinstructionsforsettingupNodechangeoften,andthebestwaytogetstartedisbyreadingFelixGeisendörfer’sbeginner’sguidetoNode,whichyoucanfindathttp://nodeguide.com/beginner.html.

Irelyonsomethird-partymodules,sorunthefollowingcommandafteryouhaveinstalledtheNode.jspackage:

npm install node-static jqtpl

Thiscommanddownloadsandinstallsthenode-staticandjqtplpackagesthatIusetodeliverstaticandtemplatedcontentintheexamples.Thecommandwillgenerateoutputsimilartothis(butyoumayseesomeadditionalwarnings,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 [email protected] ./node_modules/node-static [email protected] ./node_modules/jqtpl

Thesourcecodedownloadisorganizedbychapter.YouwillneedtocreateadirectorycalledcontentinyourNode.jsdirectoryandcopythechaptercontentintoit.Thereisn’tmuchstructuretothecontentdirectory;tokeepthingssimple,almostalloftheresourcesandlistingsareinthesamedirectory.

CautionTherearechangesintheresourcefilesbetweenchapters,somakesureyouclearyourbrowser’shistorywhenyoumovebetweenchaptercontent.

Youwillalsoneedtocopytheserver.jsfilefromthesourcecodedownloadintoyourNode.jsdirectory.ThisNodescriptisonlyforservingtheexamplesinthebook;don’trelyonitforanyotherpurpose,andcertainlydon’tuseittohostrealprojects.Onceyouhaveeverythinginplace,simplyrunthefollowingcommand:

Page 12: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER1GETTINGREADY

9

node server.js

Youwillseethefollowingoutput(orsomethingveryclosetoit):

The "sys" module is now called "util". It should have a similar interface. Ready on port 80

IfyouareusingWindows,youmaybepromptedtoallowNodetocommunicatethroughtheWindowsFirewall,whichyoushoulddo.Andwiththat,yourserverisupandrunning.Thescriptlistensforrequestsonport80.Ifyouneedtochangethis,thenlookforthefollowinglineintheserver.jsfile:

http.createServer(handleRequest).listen(80);

CautionNode.jsisveryvolatile,andnewversionsarereleasedoften.TheversionthatIhaveusedinthisbookis0.6.6,butitwillhavebeensupersededbythetimeyoureadthis.IhavestucktothemorestableNodeAPIs,butyoumightneedtomakesomeminortweakstogeteverythingworking.

IntroducingtheCheeseLuxExampleMostoftheexamplesinthisbookarebasedonawebappforafictionalcheeseretailercalledCheeseLux.Iwantedtofocusontheindividualtechniquesinthisbook,soIhavekeptthewebappassimpleaspossible.Tobeginwith,Ihavecreatedastaticwebsitethatofferslimitedproductstotheuser.Theentrypointtothesiteistheexample.htmlfile.Iuseexample.htmlforalmostallofthelistingsinthisbook.Listing1-3showstheinitialstaticversionofexample.html.

Page 13: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER1GETTINGREADY

10

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.Thesearetheproductlistingandabasketshowingauser’sselections(whichishandledinthestaticversionbybasket.html).Youcanseehowexample.htmlandbasket.htmlaredisplayedinthebrowserinFigure1-1.

Page 14: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER1GETTINGREADY

11

Figure1-1.Theexample.htmlandbasket.htmlfilesdisplayedinthebrowser

Youdon’tneedtodoanythingwiththestaticfiles,butifyoulookatthecontentsofbasket.html,forexample,youwillseethatIusetemplatestogeneratethecontentbasedonthedatasubmittedviatheHTMLforms,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>

Page 15: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER1GETTINGREADY

12

<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.ThismoduleisaNode-compliantversionofasimpletemplatelibrarythatiswidelyusedwiththejQuerylibrary.Idon’tusethisstyleoftemplateintheclient-sideexamples,butIwantedtoexplainthemeaningofthosetagsincaseyouweretemptedtopeekatthestaticcontent.

Inthenextchapter,I’llusesomebasicJavaScripttechniquestocreateamoredynamicversionofthissimpleappandthenspendtherestofthebookshowingyoumoreadvancedtechniquesyoucanusetocreatebetter,morescalable,andmoreresponsivewebappsforyourownprojects.

FontAttributionIusesomecustomwebfontsthroughoutthisbook.ThefontfilesareincludedinthesourcecodedownloadavailablefromApress.com.ThefontsIusecomefromTheLeagueofMovableType(www.theleagueofmoveabletype.com)andfromtheGoogleWebFontsservice(www.google.com/webfonts).

Page 16: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER1GETTINGREADY

13

SummaryInthischapter,Ioutlinedthecontentandstructureofthisbookandsetoutthesoftwarerequiredifyouwanttoexperimentwiththeexamplesinthisbooks.IalsointroducedtheCheeseLuxexample,whichisusedthroughoutthisbook.Inthenextchapter,I’llusesomebasictechniquestoenhancethestaticwebpagesandintroducesomeofthecoretoolsthatIusethroughoutthisbook.Fromthenon,I’llshowyouaseriesofbetter,industrial-strengthtechniquesthataretheheartofthisbook.

Page 17: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

C H A P T E R 2

15

Getting Started

Inthischapter,IamgoingtoenhancetheexamplewebappIintroducedinChapter1.Thesearetheentry-leveltechniques,andmostoftherestofthebookisdedicatedtoshowingyoudifferentwaystoimproveupontheresult.That’snottosaythattheexamplesinthischapterarenotuseful;theyareabsolutelyfineforsimplewebapps.Buttheyarenotsufficientforlargeandcomplexwebapps,whichiswhythechaptersthatfollowexplainhowyoucantakekeyconceptsfromtheworldofserver-sidedevelopmentandapplythemtoyourwebapps.

ThischapteralsoletsmesetthefoundationforsomewebappdevelopmentprinciplesthatIwillbeusingthroughoutthisbook.First,IwillberelyingonJavaScriptlibrarieswheneverpossiblesoastoavoidcreatingcodethatsomeoneelsehasproducedandmaintained.ThelibraryIwillbemakingmostuseofisjQueryinordertomakeworkingwiththeDOMAPIsimplerandeasier(IexplainsomejQuerybasicsintheexamplesinthischapters).Second,IwillbefocusingonasingleHTMLdocument.

UpgradingtheSubmitButtonTogetstarted,IamgoingtouseJavaScripttoreplacethesubmitbuttonfromthebaselineexampleinChapter1.Thebrowsercreatesthisbuttonfromaninputelementwhosetypeissubmit,andIamgoingtoswitchitoutforsomethingthatisvisuallyconsistentwiththerestofthedocument.Morespecifically,IamgoingtousejQuerytoreplacetheinputelement.

PreparingtoUsejQueryTheDOMAPIiscomprehensivebutawkwardtouse—soawkwardthatthereareanumberofJavaScriptconveniencelibrariesthatwraparoundtheDOMAPIandmakeiteasiertouse.Inmyexperience,thebestoftheselibrariesisjQuery,whichiseasytouseandactivelydevelopedandsupported.jQueryisalsothefoundationformanyotherJavaScriptlibraries,someofwhichI’llbeusinglater.jQueryisjustawrapperaroundtheDOMAPI,andthisallowstheuseoftheunderlyingDOMobjectsandmethodsifitisrequired.

YoucandownloadthejQuerylibraryfromjQuery.com.jQuery,likemostJavaScriptlibraries,isavailableintwoversions.Theuncompressedversioncontainsthefullsourcecodeandisusefulfordevelopmentanddebugging.Thecompressedversion(alsoknownastheminimizedorminifiedversion)ismuchsmallerbutisn’thuman-readable.Thesmallersizemakestheminimizedversionidealforsavingbandwidthwhenawebappisdeployedintoproduction.Bandwidthcanbeexpensiveforpopularwebapps,andanysavingsisworthmaking.

Downloadtheversionyouwantandputitinyourcontentdirectory,alongsideexample.html.I’llbeusingtheuncompressedversioninthisbook,soIhavedownloadedafilecalledjquery-1.7.1.js.

Page 18: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

16

TipIamusingtheuncompressedversionsbecausetheymakedebuggingeasier,whichyoumayfindusefulasyouexploretheexamplesinthisbook.Forrealwebapplications,youshouldswitchtotheminimizedversionpriortodeployment.

ThefilenameincludesthejQueryversion,whichis1.7.1asIwritethis.YouimportthejQuerylibraryintotheexampledocumentusingascriptelement,asshowninListing2-1.Ihaveaddedthescriptelementintheheadsectionofthedocument.

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

AnalternativetohostingthejQuerylibraryonyourownwebserversistouseapubliccontentdistributionnetwork(CDN)thathostsjQuery.ACDNisadistributednetworkofserversthatdeliverfilestotheuserusingtheserverthatisclosesttothem.ThereareacoupleofbenefitstousingaCDN.Thefirstisafasterexperiencetotheuser,becausethejQuerylibraryfileisdownloadedfromtheserverclosesttothem,ratherthanfromyourservers.Oftenthefilewon’tberequiredatall.jQueryissopopularthattheuser’sbrowsermayhavealreadycachedthelibraryfromanotherapplicationthatalsousesjQuery.ThesecondbenefitisthatnoneofyourpreciousandexpensivebandwidthisspentdeliveringjQuerytotheuser.

WhenusingaCDN,youmusthaveconfidenceintheCDNoperator.Youwanttobesurethattheuserreceivesthefiletheyaresupposedtoandthattheservicewillalwaysbeavailable.GoogleandMicrosoftbothprovideCDNservicesforjQuery(andotherpopularJavaScriptlibraries)freeofcharge.BothcompanieshavesolidexperienceofrunninghighlyavailableservicesandareunlikelytodeliberatelytamperwiththejQuerylibrary.YoucanlearnabouttheMicrosoftserviceatwww.asp.net/ajaxlibrary/cdn.ashxandabouttheGoogleserviceathttp://code.google.com/apis/libraries/devguide.html.

TheCDNapproachisn’tsuitableforapplicationsthataredeliveredtouserswithinanintranetbecauseitcausesallthebrowserstogototheInternettogetthejQuerylibrary,ratherthanaccessthelocalserver,whichisgenerallycloserandfasterandhaslowerbandwidthcosts.

So,let’sjumprightinandusejQuerytohidetheexistinginputelementandaddsomethingelsein

itsplace.Listing2-2showshowthisisdone.

Page 19: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

17

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,ratherthanloadinganexternalJavaScriptfile.IhavedonethisbecauseitmakesiteasiertoshowyouthechangesIammaking.UsinginlinecodeisnotajQueryrequirement,andyoucanputyourjQuerycode

Page 20: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

18

inexternalfilesifyouprefer.ThereisalotgoingoninthefourJavaScriptstatementsinthescriptelement,soI’llbreakthingsdownstep-by-stepinthefollowingsections.

UnderstandingtheReadyEventAttheheartofjQueryisthe$function,whichisaconvenientshorthandtobeginusingjQueryfeatures.ThemostcommonwaytousejQueryistotreatthe$asaJavaScriptfunctionandpassaCSSselectororoneormoreDOMobjectsasarguments.Usingthe$functionisverycommonwithjQuery.Ihaveuseditthreetimesinfourlinesofcode,forexample.

The$functionreturnsajQueryobjectonwhichyoucancalljQuerymethods.ThejQueryobjectisawrapperaroundtheelementsyouselected,andifyoupassaCSSselectorastheargument,thejQueryobjectwillcontainalloftheelementsinthedocumentthatmatchtheselectoryouspecify.

TipThisisoneofthemainadvantagesofjQueryoverthebuilt-inDOMAPI:youcanselectandmodifymultipleelementsmoreeasily.ThemostrecentversionsoftheDOMAPI(includingtheonethatispartofHTML5)providesupportforfindingelementsusingselectors,butjQuerydoesitmoreconciselyandelegantly.

ThefirsttimeIusethe$functioninthelisting,Ipassinthedocumentobjectastheargument.ThedocumentobjectistherootnodeoftheelementhierarchyintheDOM,andIhaveselecteditwiththe$functionsothatIcancallthereadymethod,ashighlightedinListing2-3.

Listing2-3.SelectingtheDocumentandCallingthereadyMethod

... <script> $(document).ready(function() { ...other JavaScript statements... }) </script> ...

BrowsersexecuteJavaScriptcodeassoonastheyfindthescriptelementsinthedocument.ThisgivesusaproblemwhenyouwanttomanipulatetheelementsintheDOM,becauseyourcodeisexecutedbeforethebrowserhasparsedtherestoftheHTMLdocument,discoveredtheelementsthatyouwanttoworkwith,andaddedobjectstotheDOMtorepresentthem.AtbestyourJavaScriptcodedoesn’twork,andatworstyoucauseanerrorwhenthishappens.Thereareanumberofwaystoworkaroundthis.Thesimplestsolutionistoplacethescriptelementattheendofthedocumentsothatthebrowserdoesn’tdiscoverandexecuteyourJavaScriptcodeuntiltherestoftheHTMLhasbeenprocessed.AmoreelegantapproachistousethejQueryreadymethod,whichishighlightedinthelistingjustshown.

YoupassaJavaScriptfunctionastheargumenttothereadymethod,andjQuerywillexecutethisfunctiononcethebrowserhasprocessedalloftheelementsinthedocument.Usingthereadymethodallowsyoutoplaceyourscriptelementsanywhereinthedocument,safeintheknowledgethatyourcodewon’tbeexecuteduntiltherightmoment.

www.allitebooks.com

Page 21: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

19

CautionAcommonmistakeistoforgettowraptheJavaScriptstatementstobeexecutedinafunction,whichcausesanoddeffect.Ifyoupassasinglestatementtothereadymethod,thenitwillbeexecutedassoonasthebrowserprocessesthescriptelement.Ifyoupassmultiplestatements,thenthebrowserwillusuallyreportaJavaScripterror.

Thereadymethodcreatesahandlerforthereadyevent.I’llshowyoumoreofthewaythatjQuerysupportseventslaterinthischapter.Thereadyeventisavailableonlyforthedocumentobject,whichiswhyyouwillseethestatementshighlightedinthelistinginalmosteverywebappthatusesjQuery.

SelectingandHidingtheInputElementNowthatIhavedelayedtheexecutionoftheJavaScriptcodeuntiltheDOMisready,Icanturntothenextstepinmytask,whichistohidetheinputelementthatsubmitstheform.Listing2-4highlightsthestatementfromtheexamplethatdoesjustthis.

Listing2-4.SelectingandHidingtheinputElement

... <script> $(document).ready(function() { $('#buttonDiv input:submit').hide(); $('<a href=#>Submit Order</a>').appendTo("#buttonDiv"); }) </script> ...

Thisisaclassictwo-partjQuerystatement:firstIselecttheelementsIwanttoworkwith,andthenIapplyajQuerymethodtomodifytheselectedelements.YoumaynotrecognizetheselectorIhaveusedbecausethe:submitpartisoneoftheselectorsthatjQuerydefinesinadditiontothoseintheCSSspecification.Table2-1containsthemostusefuljQuerycustomselectors.

Page 22: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

20

CautionThejQuerycustomselectorscanbeextremelyuseful,buttheyhaveaperformanceimpact.Whereverpossible,jQueryusesthenativebrowsersupportforfindingelementsinthedocument,andthisisusuallyprettyquick.However,jQueryhastoprocessthecustomselectorsdifferently,sincethebrowserdoesn’tknowanythingaboutthem,andthistakeslongerthanthenativeapproach.Thisperformancedifferencedoesn’tmatterformostwebapps,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

Page 23: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

21

InListing2-4,myselectormatchesanyinputelementwhosetypeissubmitandthatisadescendantoftheelementwhoseidattributeisbuttonDiv.Ididn’tneedtobequitesoprecisewiththeselector,giventhatitistheonlysubmitelementinthedocument,butIwantedtodemonstratethejQuerysupportforselectors.The$functionreturnsajQueryobjectthatcontainstheselectedelements,althoughthereisonlyoneelementthatmatchestheselectorinthiscase.

Havingselectedtheelement,Ithencallthehidemethod,whichchangesthevisibilityoftheselectedelementsbysettingtheCSSdisplaypropertytonone.Theinputelementislikethisbeforethemethodcall:

<input type="submit">

andistransformedlikethisafterthemethodcall:

<input type="submit" style="display: none; ">

Thebrowserwon’tshowelementswhosedisplaypropertyisnoneandsotheinputelementbecomesinvisible.

TipThecounterparttothehidemethodisshow,whichremovesthedisplaysettingandreturnstheelementtoitsvisiblestate.Idemonstratetheshowmethodlaterinthischapter.

InsertingtheNewElementNext,Iwanttoinsertanewelementintothedocument.Listing2-5highlightsthestatementintheexamplethatdoesthis.

Listing2-5.AddingaNewelementtotheDocument

... <script> $(document).ready(function() { $('#buttonDiv input:submit').hide(); $('<a href=#>Submit Order</a>').appendTo("#buttonDiv"); }) </script> ...

Inthisstatement,IhavepassedanHTMLfragmentstringtothejQuery$function.ThiscausesjQuerytoparsethefragmentandcreateasetofobjectstorepresenttheelementsitcontains.TheseelementobjectsarethenreturnedtomeinajQueryobject,justasifIhadselectedelementsfromthedocumentitself,exceptthatthebrowserdoesn’tyetknowabouttheseelementsandtheyarenotyetpartoftheDOM.

ThereisonlyoneelementintheHTMLfragmentinthislisting,sothejQueryobjectcontainsanaelement.ToaddthiselementtotheDOM,IcalltheappendTomethodonthejQueryobject,passinginaCSSselector,whichtellsjQuerywhereinthedocumentIwanttheelementtobeinserted.

TheappendTomethodinsertsmynewelementasthelastchildoftheelementsmatchedbytheselector.Inthiscase,IspecifiedthebuttonDivelement,whichmeansthattheelementsinmyHTMLfragmentareinsertedalongsidethehiddeninputelement,likethis:

Page 24: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

22

... <div id="buttonDiv"> <input type="submit" style="display: none; "> <a href="#">Submit Order</a> </div> ...

TipIftheselectorthatIpassedtotheappendTomethodhadmatchedmultipleelements,thenjQuerywouldduplicatetheelementsfromtheHTMLfragmentandinsertacopyasthelastchildofeverymatchedelement.

jQuerydefinesanumberofmethodsthatyoucanusetoinsertchildelementsintothedocument,andthemostusefulofthesearedescribedinTable2-2.Whenyouappendelements,theybecomethelastchildrenoftheirparentelement.Whenyouprependelements,theybecomethefirstchildrenoftheirparents.(I’llexplainwhytherearetwoappendandtwoprependmethodslaterinthischapter.)

Table2-2.jQueryMethodsforInsertingElementsintheDocument

Method Description

append(HTML) append(jQuery)

InsertsthespecifiedelementsasthelastchildrenofalltheelementsintheDOM

prepend(HTML) prepend(jQuery)

InsertsthespecifiedelementsasthefirstchildrenofalltheelementsintheDOM

appendTo(HTML) appendTo(jQuery)

InsertstheelementsinthejQueryobjectasthelastchildrenoftheelementsspecifiedbytheargument

prependTo(HTML) prependTo(jQuery)

InsertstheelementsinthejQueryobjectasthefirstchildrenoftheelementsspecifiedbytheargument

ApplyingaCSSClassInthepreviousexample,Iinsertedanaelement,butIdidnotassignittoaCSSclass.Listing2-6showshowIcancorrectthisomissionbymakingacalltotheaddClassmethod.

Page 25: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

23

Listing2-6.ChainingjQueryMethodCalls

... <script> $(document).ready(function() { $('#buttonDiv input:submit').hide(); $('<a href=#>Submit Order</a>').appendTo("#buttonDiv").addClass("button"); }) </script> ...

NoticehowIhavesimplyaddedthecalltotheaddClassmethodtotheendofthestatement.Thisisknownasmethodchaining,andalibrarythatsupportsmethodchainingissaidtohaveafluentAPI.

MostjQuerymethodsreturnthesamejQueryobjectonwhichthemethodwascalled.Intheexample,IcreatethejQueryobjectbypassinganHTMLfragmenttothe$function.ThisproducesajQueryobjectthatcontainsanaelement.TheappendTomethodinsertstheelementintothedocumentandreturnsajQueryobjectthatcontainsthesameaelementasitsresult.Thisallowsmetomakefurthermethodcalls,suchastheonetoaddClass.FluentAPIscantakeawhiletogetusedto,buttheyenableconciseandexpressivecodeandreduceduplication.

TheaddClassmethodaddstheclassspecifiedbytheargumenttotheselectedelements,likethis:

... <div id="buttonDiv"> <input type="submit" style="display: none; "> <a href="#" class="button">Submit Order</a> </div> ...

Thea.buttonclassisdefinedinstyles.cssandbringstheappearanceoftheaelementintolinewiththerestofthedocument.

UNDERSTANDING METHOD PAIRS AND METHOD CHAINING

IfyoulookatthemethodsdescribedinTable2-2,youwillseethatyoucanappendorprependelementsintwoways.TheelementsyouareinsertingeithercanbecontainedinthejQueryobjectonwhichyoucallamethodorcanbeinthemethodargument.jQueryprovidesdifferentmethodssoyoucanselectwhichelementsarecontainedinthejQueryobjectformethodchaining.Inmyexample,IusedtheappendTomethod,whichmeansIcanarrangethingssothatthejQueryobjectcontainstheelementparsedfromtheHTMLfragment,allowingmetochainthecalltotheaddClassmethodandhavetheclassappliedtotheaelement.

Theappendmethodreversestherelationshipbetweentheparentandchildelements,likethis:

$('#buttonDiv').append('<a href=#>Submit Order</a>').addClass("button");

Inthisstatement,IselecttheparentelementandprovidetheHTMLfragmentasthemethodargument.TheappendmethodreturnsajQueryobjectthatcontainsthebuttonDivelement,sotheaddClasstakeseffectontheparentdivelementratherthanthenewaelement.

Page 26: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

24

Torecap,Ihavehiddentheoriginalinputelement,addedanaelement,and,finally,assignedtheaelementtothebuttonclass.YoucanseetheresultinFigure2-1.

Figure2-1.Replacingthestandardformsubmitbutton

Withfourlinesofcode(onlytwoofwhichmanipulatetheDOM),Ihaveupgradedthestandardsubmitbuttontosomethingconsistentwiththerestofthewebapp.AsIsaidatthestartofthischapter,alittlecodecanleadtosignificantenhancements.

RespondingtoEventsIamnotquitedonewiththenewaelement.ThebrowserknowsthataninputelementwhosetypeattributeissubmitshouldsubmittheHTMLformtotheserver,anditperformsthisactionautomaticallywhenthebuttonisclicked.

TheaelementthatIaddedtotheDOMlookslikeabutton,butthebrowserdoesn’tknowwhattheelementisforandsodoesn’tapplythesameautomaticaction.IhavetoaddsomeJavaScriptcodethatwillcompletetheeffectandmaketheaelementbehavelikeabuttonandnotjustlooklikeone.

Youdothisbyrespondingtoevents.Aneventisamessagethatissentbythebrowserwhenthestateofanelementchanges,forexample,whentheuserclickstheelementormovesthemouseoverit.YoutellthebrowserwhicheventsyouareinterestinginandprovideJavaScriptcallbackfunctionsthatareexecutedwheneventoccurs.Aneventissaidtohavebeentriggeredwhenitissentbythebrowser,andthecallbackfunctionsareresponsibleforhandlingtheevent.Inthefollowingsections,I’llshowyouhowtohandleeventstocompletethefunctionalityofthesubstitutebutton.

4

Page 27: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

25

HandlingtheClickEventThemostimportantforthisexampleisclick,whichistriggeredwhentheuserpressesandreleasesthemousebutton(inotherwords,whentheuserclicks)anelement.Forthisexample,IwanttohandletheclickeventbysubmittingtheHTMLformtotheserver.TheDOMAPIprovidessupportfordealingwithevents,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>

Page 28: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

26

<div id="buttonDiv"> <input type="submit" /> </div> </form> </body> </html>

jQueryprovidessomehelpfulmethodsthatmakehandlingcommoneventssimple.Theseeventsarenamedaftertheevent;so,theclickmethodregistersthecallbackfunctionpassedasthemethodargumentasahandlerfortheclickevent.Ihavechainedthecalltotheclickeventtotheothermethodsthatcreateandformattheaelement.Tosubmittheform,Iselecttheformelementbytypeandcallthesubmitmethod.That’sallthereistoit.Inowhavethebasicfunctionalityofthebuttoninplace.Notonlydoesithavethesamevisualstyleastherestofthewebapp,butclickingthebuttonwillsubmittheformtotheserver,justastheoriginalbuttondid.

HandlingMouseHoverEventsTherearetwoothereventsthatIwanttohandletocompletethebuttonfunctionality;theyaremouseenterandmouseleave.Themouseentereventistriggeredwhenthemousepointerismovedovertheelement,andthemouseleaveeventistriggeredthemouseleavestheelement.

Iwanttohandletheseeventstogivetheuseravisualcuethatthebuttoncanbeclicked,andIdothisbychangingthestyleofthebuttonwhenthemouseisovertheelement.TheeasiestwaytohandletheseeventsistousethejQueryhovermethod,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.Thefirstfunctionisexecutedwhenthemouseentereventistriggered,andthesecondfunctionistriggeredinresponsetothemouseleaveevent.Inthisexample,IhaveusedthesefunctionstoaddandremovethebuttonHoverclassfromtheaelement.ThisclasschangesthevalueoftheCSSbackground-colorpropertytohighlightthebuttonwhenthemouseispositionedabovetheelement.YoucanseetheeffectinFigure2-2.

Page 29: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

27

Figure2-2.Usingeventstoapplyaclasstoanelement

UsingtheEventObjectThetwofunctionsthatIpassedasargumentstothehovermethodinthepreviousexamplearelargelythesame.Icancollapsethesetwofunctionsintoasinglehandlerthatcanprocessbothevents,asshowninListing2-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.ThisargumentisanEventobjectprovidedbythebrowsertogiveyouinformationabouttheeventyouarehandling.IhaveusedtheEvent.typepropertytodifferentiatebetweenthetypesofeventsthatmyfunctionexpects.Thetypepropertyreturnsastringthatcontainstheeventname.Iftheeventnameismouseenter,thenIcalltheaddClassmethod.Ifnot,IcalltheremoveClassmethodthathastheeffectofremovingthespecifiedclassfromtheclassattributeoftheelementsinthejQueryobject,theoppositeeffectoftheaddClassmethod.

Page 30: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

28

DealingwithDefaultActionsTomakelifeeasierfortheprogrammer,thebrowserperformssomeactionsautomaticallywhencertaineventsaretriggeredforspecificelementtypes.Theseareknownasdefaultactions,andtheymeanyoudon’thavetocreateeventhandlersforeverysingleeventandelementinanHTMLdocument.Forexample,thebrowserwillnavigatetotheURLspecifiedbythehrefattributeofanaelementinresponsetotheclickevent.Thisisthebasisfornavigationinawebpage.

Icheatedalittlebysettingthehrefattributeto#.ThisisacommontechniquewhendefiningelementswhoseactionsaregoingtobemanagedbyJavaScriptbecausethebrowserwon’tnavigateawayfromthecurrentdocumentwhenthedefaultactionisperformed.Inotherwords,Idon’thavetoworryaboutthedefaultactionbecauseitdoesn’treallydoanythingthattheuserwillnotice.

Defaultactionscanbemoreimportantwhenyouneedtochangethebehavioroftheelementandyoucan’tdolittletrickslikeusing#asaURL.Listing2-10providesademonstration,whereIhavechangedthehrefattributefortheaelementtoarealwebpage.Ihaveusedtheattrmethodtosetthehrefattributeoftheaelementtohttp://apress.com.Withthismodification,clickingtheelementdoesn’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,acalltothepreventDefaultmethodontheEventobjectpassedtotheeventhandlerfunctionisrequired.Thisdisablesthedefaultactionfortheevent,meaningthatonlythecodeintheeventhandlerfunctionwillbeused.YoucanseetheuseofthismethodinListing2-11.

www.allitebooks.com

Page 31: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

29

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,sointhislisting,IneedonlytocallthepreventDefaultmethodwhenhandlingtheclickevent.WhenIclicktheelementnow,theformissubmitted,andthehrefattributevaluedoesn’thaveanyeffect.

AddingDynamicBasketDataYouhaveseenhowyoucanimproveawebapplicationsimplybyaddingandmodifyingelementsandhandlingevents.Inthissection,Igoonestepfurthertodemonstratehowyoucanusethesesimpletechniquestocreateamoreresponsiveversionofthecheeseshopbyincorporatingtheinformationdisplayedinthebasketphasealongsidetheproductselection.IhavecalledthisadynamicbasketbecauseIwillbeupdatingtheinformationshowntouserswhentheychangethequantitiesofindividualcheeseproducts,ratherthanthestaticbasket,whichisshownwhenuserssubmittheirselectionsusingtheunenhancedversionofthiswebapp.

AddingtheBasketElementsThefirststepistoaddtheadditionalelementsIneedtothedocument.IcouldaddtheelementsusingHTMLfragmentsandtheappendTomethod,butforvarietyIamgoingtouseanothertechnique,knownaslatentcontent.LatentcontentreferstoHTMLelementsthatareinthedocumentbutarehiddenusingCSSandarerevealedandmanagedusingJavaScript.Thoseuserswhodon’thaveJavaScriptenabledwon’tseetheelementsandwillgetthebasicfunctionality,butonceIrevealtheelementsandsetupmyeventhandling,thoseuserswithJavaScriptwillgetaricherandmorepolishedexperience.Listing2-12showstheadditionofthelatentcontenttotheHTMLdocument.

Page 32: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

30

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>

Page 33: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

31

<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; } ...

IshowedyouearlierinthechapterthatthejQueryhidemethodsetstheCSSdisplaypropertytononetohideelementsfromtheuser,andIhavefollowedthesameapproachwhensettingupthisclass.Theelementsareinthedocumentbutnotvisibletotheuser.

ShowingtheLatentContentNowthatthelatentelementsareinplace,IcanworkwiththemusingjQuery.Thefirststepistorevealthemtotheuser.SinceIammanipulatingtheelementsusingJavaScript,theywillberevealedonlytouserswhohaveJavaScriptenabled.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");

Page 34: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

32

} else { elem.removeClass("buttonHover"); } }); $('.latent').show(); }) </script> ...

Thehighlightedstatementselectsalloftheelementsthataremembersofthelatentclassandthencallstheshowmethod.Theshowmethodaddsastyleattributetoeachselectedelementthatsetsthedisplaypropertytoinline,whichhastheeffectofrevealingtheelements.Theelementsarestillmembersofthelatentclass,butvaluesdefinedinastyleattributeoverridethosethataredefinedinastyleelement,andsotheelementsbecomevisible.

RespondingtoUserInputTocreateadynamicbasket,Iwanttobeabletodisplaysubtotalsforeachitemandanoveralltotalwhenevertheuserchangesaquantityforaproduct.IamgoingtohandletwoeventstogettheeffectIwant.Thefirsteventischange,whichistriggeredwhentheuserentersanewvalueandthenmovesthefocustoanotherelement.Thesecondeventiskeyup,whichistriggeredwhentheuserreleasesakey,havingpreviouslypressedit.ThecombinationofthesetwoeventsmeansIcanbeconfidentthatIwillbeabletorespondsmoothlytonewvalues.jQuerydefineschangeandkeyupmethodsthatIcoulduseinthesamewayIusedtheclickmethodearlier,butsinceIwanttohandlebotheventsinthesameway,Iamgoingtousethebindmethodinstead,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");

Page 35: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

33

} }) $('.latent').show(); $('input').bind("change keyup", function() { var subtotal = $(this).val() * priceData[this.name]; $(this).siblings("span").children("span").text(subtotal) }) }) </script> ...

TheadvantageofthebindmethodisthatitletsmehandlemultipleeventsusingthesameanonymousJavaScriptfunction.Todothis,IhaveselectedtheinputelementsinthedocumenttogetajQueryobjectandcalledthebindmethodonit.Thefirstargumenttothebindmethodisastringcontainingthenamesoftheeventstohandle,whereeventnamesareseparatedbythespacecharacter.Thesecondargumentisthefunctionthatwillhandletheeventswhentheyaretriggered.Thereareonlytwostatementsintheeventhandlerfunction,buttheyareworthunpackingbecausetheycontainaninterestingmixofjQuery,theDOMAPI,andpureJavaScript.

TipHandlingtwoeventslikethismeansthatmycallbackfunctionmayendupbeinginvokedwhenitdoesn’treallyneedtobe.Forexample,iftheuserpressestheTabkey,thefocuswillchangetothenextelement,andboththechangeandkeyupeventswillbetriggered,eventhoughthevalueintheinputelementhasn’tchanged.Itendtowardacceptingthisduplicationasthecostofensuringafluiduserexperience.I’drathermyfunctionwasexecutedmoreoftenthanreallyneededandnotmissanyuserinteraction.

CalculatingtheSubtotalThefirststatementinthefunctionisresponsibleforcalculatingthesubtotalforthecheeseproductwhoseinputvaluehaschanged.Hereisthestatement:

var subtotal = $(this).val() * priceData[this.name];

WhenhandlinganeventwithjQuery,youcanusethevariablecalledthistorefertotheelementthattriggeredtheevent.ThethisvariableisanHTMLElementobject,whichiswhattheDOMAPIusestorepresentelementsinthedocument.ThereareacoresetofpropertiesdefinedbytheHTMLElement,themostimportantofwhicharedescribedinTable2-3.

Page 36: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

34

Table2-3.BasicHTMLElementProperties

Property Description

className Getsorsetsthelistofclassesthattheelementbelongsto

id Getsorsetsthevalueoftheidattribute

tagName Returnsthetagname(indicatingtheelementtype)

Thecorepropertiesaresupplementedtoaccommodatetheuniquecharacteristicsofdifferent

elementtypes.Anexampleofthisisthenameproperty,whichreturnsthevalueofthenameattributeonthoseelementsthatsupportit,includingtheinputelement.IhaveusedthispropertyonthethisvariabletogetthenameoftheinputelementsothatIcan,inturn,useittogetavaluefromthepriceDataobjectthatIaddedtothescript:

var subtotal = $(this).val() * priceData[this.name];

ThepriceDataobjectisasimpleJavaScriptobjectthathasonepropertycorrespondingtoeachkindofcheeseandwherethevalueofeachpropertyisthepriceforthecheese.

ThethisvariablecanalsobeusedtocreatejQueryobjects,likethis:

var subtotal = $(this).val() * priceData[this.name];

BypassinganHTMLElementobjectastheargumenttothejQuery$function,IhavecreatedajQueryobjectthatactsjustasthoughIhadselectedtheelementusingaCSSselector.ThisallowsmetoeasilyapplyjQuerymethodstoobjectsfromtheDOMAPI.Inthisstatement,Icallthevalmethod,whichreturnsthevalueofthevalueattributeofthefirstelementinthejQueryobject.

TipThereisonlyoneelementinmyjQueryobject,butjQuerymethodsaredesignedtoworkwithmultipleelements.Whenyouuseamethodlikevaltoreadsomevaluefromtheelement,yougetthevaluefromthefirstelementintheselection,butwhenyouusethesamemethodtosetthevalue(bypassingthevalueasanargument),alloftheselectedelementsaremodified.

Usingthethisvariable,Ihavebeenabletogetthevalueoftheinputelementthattriggeredtheeventandthepricefortheproductassociatedwithit.Ithenmultiplythepriceandthequantitytogethertodeterminethesubtotal,whichIassigntoalocalvariablecalled,simplyenough,subtotal.

DisplayingtheSubtotalThesecondstatementinthehandlerfunctionisresponsiblefordisplayingthesubtotaltotheuser.Thisstatementalsooperatesintwoparts.Thefirstpartselectstheelementthatwillbeusedtodisplaythevalue:

$(this).siblings("span").children("span").text(subtotal)

Page 37: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

35

Onceagain,IcreateajQueryobjectusingthethisvariable.Imakeacalltothesiblingsmethod,whichreturnsajQueryobjectthatcontainsanysiblingtotheelementsintheoriginaljQueryobjectthatmatchesthespecifiedCSSselector.ThismethodreturnsajQueryobjectthatcontainsthelatentspanelementnexttotheinputelementthattriggeredtheevent.

Ichainacalltothechildrenmethod,whichreturnsajQueryobjectthatcontainsanychildrenoftheelementinthepreviousjQueryobjectthatmatchthespecifiedselector.IendupwithajQueryobjectthatcontainsthenestedspanelement.Icouldhavesimplifiedtheselectorsinthisexample,butIwantedtodemonstratehowjQuerysupportsnavigationthroughtheelementsinadocumentandhowthecontentsofthejQueryobjectinachainofmethodcallschanges.ThesechangesaredescribedinTable2-4.

Table2-4.BasicHTMLElementProperties

Method Call Contents of jQuery Object

$(this) Theinputelementthattriggeredtheevent

.siblings("span") Thespanelementthatisasiblingtotheinputelementthattriggeredtheevent

.children("span") Thespanelementthatisachildofthespanelementthatisasiblingtotheinputelementthattriggeredtheevent

Bycombiningmethodcallslikethis,Iamabletonavigatethroughtheelementhierarchytocreatea

jQueryobjectthatcontainspreciselytheelementorelementsIwanttoworkwith,inthiscase,thechildofasiblingtowhicheverelementtriggeredanevent.

Thesecondpartofthestatementisacalltothetextmethod,whichsetsthetextcontentoftheelementsinajQueryobject.Inthiscase,thetextisthevalueofthesubtotalvariable:

$(this).siblings("span").children("span").text(subtotal)

Thenetresultisthatthesubtotalforacheeseisupdatedassoonasauserchangesthequantityrequired.

CalculatingtheOverallTotalTocompletethebasket,Ineedtogenerateanoveralltotaleachtimeasubtotalchanges.Ihavedefinedanewfunctioninthescriptelementandaddedacalltoitintheeventhandlerfunctionfortheinputelements.Listing2-15showstheadditions.

Page 38: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

36

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.Iusethisvariabletosumtheindividualsubtotals.Thenextstatementisthemostinterestingoneinthisfunction.Thefirstpartofthestatementselectsasetofelements:

Page 39: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

37

... $('span.subtotal span').not('#total').each(function(index, elem) { ...

Istartbyselectingallspanelementsthataredescendantsofspanelementsthatarepartofthesubtotalclass.Thisisanotherwayofselectingthesubtotalelements.Ithenusethenotmethodtoremoveelementsfromtheselection.Inthiscase,Iremovetheelementwhoseidistotal.IdothisbecauseIdefinedthesubtotalandtotalelementsusingthesameclassesandstyles,andIdon’twantthecurrenttotaltobeincludedwhencalculatinganewtotal.

Havingselectedtheitems,Ithenusetheeachmethod.ThismethodcallsafunctiononceforeachelementinajQueryobject.TheargumentstothefunctionaretheindexofthecurrentelementintheselectionandtheHTMLElementobjectthatrepresentstheelementintheDOM.

Igetthecontentofeachsubtotalelementusingthetextmethod.IcreateajQueryobjectbypassingtheHTMLElementobjectasanargumenttothe$function,justasIdidwiththethisvariableearlierinthischapter.

Thetextmethodreturnsastring,soIusetheJavaScriptNumberfunctiontocreateanumericvaluethatIcanaddtotherunningtotal:

total += Number($(elem).text());

Finally,Iselectthetotalelementandusethetextmethodtodisplaytheoveralltotal:

$('#total').text("$" + total);

Theeffectofaddingthisfunctionisthatachangeinthequantityforacheeseisimmediatelyreflectedinthetotal,aswellasintheindividualsubtotals.

ChangingtheFormTargetByaddingadynamicbasket,Ihavepulledthefunctionalityofthebasketwebpageintothemainpageoftheapplication.Itdoesn’tmakesensetosendJavaScript-enableduserstothebasketwebpagewhentheysubmittheform,becauseitjustduplicatedinformationtheyhavealreadyseen.Iamgoingtochangethetargetoftheformelementsothatsubmittingtheformgoesstraighttotheshippingpage,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();

Page 40: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

38

}).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)andcalltheattrmethodtosetanewvaluefortheactionattribute.Theuseristakentotheshippingdetailspagewhentheformissubmitted,skippingthebasketpageentirely.YoucanseetheeffectinFigure2-3.

www.allitebooks.com

Page 41: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

39

Figure2-3.Changingtheflowoftheapplication

Asthisexampledemonstrates,youcanchangetheflowofawebapplicationaswellastheappearanceandinteractivityofindividualpages.Ofcourse,theback-endservicesneedtounderstandthevariouspathsthatdifferentkindsofusercanfollowthroughawebapp,butthisiseasytoachievewithalittleforethoughtandplanning.

UnderstandingProgressiveEnhancementThetechniquesIhavedemonstratedinthischapterarebasicbutveryeffective.ByusingJavaScripttomanagetheelementsintheDOMandrespondtoevents,Ihavebeenabletomaketheexamplewebappmoreresponsivefortheuser,provideusefulandtimelyinformationaboutthecostoftheuser’sproductselections,andstreamlinetheflowoftheappitself.

But—andthisisimportant—becausethesechangesaredonethroughJavaScript,thebasicnatureandstructureofthewebappremainunchangedfornon-JavaScriptusers.Figure2-4showsthemainwebapppagewhenJavaScriptisenabledanddisabled.

Page 42: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

40

Figure2-4.ThewebappasshownwhenJavaScriptisdisabledandenabled

Theversionthatnon-JavaScriptusersexperienceremainsfullyfunctionalbutisclunkiertouseandrequiresmorestepstoplaceanorder.

Creatingabaseleveloffunctionalityandthenselectivelyenrichingitisanexampleofprogressiveenhancement.Progressiveenhancementisn’tjustabouttheavailabilityofJavaScript;itencompassesselectiveenrichmentbasedonanyfactor,suchastheamountofbandwidth,thetypeofbrowser,oreventhelevelofexperienceoftheuser.However,whencreatingwebapps,themostcommonformofprogressiveenhancementisdrivenbywhethertheuserhasJavaScriptenabled.

TipAsimilartermtoprogressiveenhancementisgracefuldegradation.Formypurposesinthisbook,progressiveenhancementandgracefuldegradationarethesame—thenotionthatthecorecontentandfeaturesofawebapplicationareavailabletoallusers,irrespectiveofthecapabilitiesofauser’sbrowser.

Ifyoudon’twanttosupportnon-JavaScriptbrowsers,thenyoushouldmakeitobvioustonon-JavaScriptvisitorsthatthereisaproblem.Theeasiestwaytodothisisbyusingthenoscriptandmetaelementstoredirectthebrowsertoapagethatexplainsthesituation,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...

Page 43: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

41

</script> <noscript> <meta http-equiv="refresh" content="0; noscript.html"/> </noscript> </head> ...

Thiscombinationofelementsredirectstheusertoapagecallednoscript.html,whichisanHTMLdocumentthattellstheuserthatIrequireJavaScript(and,obviously,doesn’trelyonJavaScriptitself).YoucanfindthispageinthesourcecodedownloadthataccompaniesthisbookandseetheresultinFigure2-5.

Figure2-5.EnforcingaJavaScript-onlypolicyinawebapp

ItistemptingtorequireJavaScript,butIrecommendcaution;youmightbesurprisedbyhowmanyusersdon’tenableJavaScriptorsimplycan’t.Thisisespeciallytrueforusersinlargecorporations,wherecomputersareusuallylockeddownandwherefeaturesthatarecommoninthegeneralpopulationaredisabledinthenameofsecurity,including,sadly,JavaScriptinbrowsers.Somewebappsjustdon’tmakesensewithoutJavaScript,butgivecarefulthoughttothepotentialusers/customersyouwillbeexcludingbeforedecidingthatyouarebuildingoneofthem.

NoteThisisabookaboutbuildingwebappswithJavaScript,soIamnotgoingtomaintainprogressiveenhancementinthechaptersthatfollow.Don’ttakethatasanendorsementofaJavaScript-onlypolicy.Inmyownprojects,Itrytosupportnon-JavaScriptuserswheneverpossible,evenwhenitrequiresalotofadditionaleffort.

Page 44: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

42

RevisitingtheButton:UsingaUIToolkitIwanttofinishthischapterbyshowingyouadifferentapproachtoobtainingoneoftheresultsinthischapter:creatingavisuallyconsistentbutton.ThetechniquesIusedpreviouslydemonstratedhowyoucanmanipulatetheDOMandrespondtoeventstotailortheappearanceandbehaviorofelements,whichisthemainpremiseinthischapter.

Thatsaid,forprofessionaldevelopment,itagoodprincipletoneverwritewhatyoucanobtainfromagoodJavaScriptlibrary,andwhenIwanttocreatevisuallyrichelements,IuseaUItoolkit.Inthissection,I’llshowyouhoweasyitistocreateacustombuttonwithjQueryUI,whichisproducedbythejQueryteamandisoneofthemostwidelyusedJavaScriptUItoolkitsavailable.

SettingUpjQueryUISettingupjQueryUIisamultistageprocess.Thefirststageistocreateatheme,whichdefinestheCSSstylesthatareusedbythejQueryUIwidgets(whichisthenamegiventothestyledelementsthataUItoolkitcreates).Tocreateatheme,gotohttp://jqueryui.com,clicktheThemesbutton,expandeachsectionontheleftsideofthescreen,andspecifythestylesyouwant.Asyoumakechanges,thesamplewidgetsontherightsideofthescreenwillupdatetoreflectthenewsettings.Ittookmeaboutfiveminutes(andabitoftrialanderror)tocreateathemethatmatchestheappearanceoftheexamplewebapp.IhaveincludedthethemeIcreatedinthesourcecodedownloadforthisbookifyoudon’twanttocreateyourown.

TipIfyoudon’twanttocreateacustomtheme,youcanselectapredefinedstylefromthegallery.Thiscanbeusefulifyouarenottryingtomatchanexistingappdesign,althoughthecolorsusedinsomeofgallerystylesarequitealarming.

Whenyouaredone,clicktheDownloadThemebutton.YouwillseeascreenthatallowsyoutoselectwhichcomponentsofjQueryUIareincludedinthedownload.YoucancreateasmallerdownloadifyougetintothedetailofjQueryUI,butforthisbookensurethatallofthecomponentsareselectedandclicktheDownloadbutton.Yourbrowserwilldownloada.zipfilethatcontainsthejQueryUIlibrary,theCSSthemeyoucreated,andsomesupportingimages.

Thesecondpartofthesetupistocopythefollowingfilesfromthe.zipfileintothecontentdirectoryoftheNode.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,thecurrentversionis1.8.16,butyouwillprobablyhavealaterversionbythetimethisbookgoesintoprint.

Page 45: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

43

TipOnceagain,IamusingtheuncompressedversionsoftheJavaScriptfiletomakedebuggingeasier.Youwillfindtheminimizedversioninthejsfolderofthe.zipfile.

CreatingajQueryUIButtonNowthatjQueryUIissetup,IcanuseitinmyHTMLdocumenttocreateabuttonwidgetandsimplifymycode.Listing2-18showstheadditionsrequiredtoimportjQueryUIintothedocumentandtocreateabutton.

ImportingjQueryUIissimplyamatterofaddingascriptelementtoimporttheJavaScriptfileandalinkelementtoimporttheCSSfile.Youdon’tneedtoexplicitlyreferencetheimagesdirectory.

TipNoticethatthescriptelementthatimportsthejQueryUIJavaScriptfilecomesaftertheonethatimportsjQuery.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(); })

Page 46: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

44

$('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,IusejQuerytoselecttheelementIwanttomodifyandcallthebuttonmethod,asfollows:

$('#buttonDiv input:submit').button()

Withasinglemethodcall,jQueryUIchangestheappearanceofthelabelsandhandlesthehighlightingwhenthemousehoversoverthebutton.Idon’tneedtoworryabouthandlingtheclickeventinthiscase,becausethedefaultactionforasubmitinputelementistosubmittheform,whichisexactlywhatIwanttohappen.

Ihavemadeoneadditionalmethodcall,usingthecssmethod.ThismethodappliesaCSSpropertydirectlytotheselectedelementsusingthestyleattribute,andIhaveusedittosetthefont-familypropertyontheinputelement.ThejQueryUIthemesystemdoesn’thavemuchsupportfordealingwithfontsandgeneratesitswidgetsusingasinglefontfamily.IhavesetupwebfontsfromtheGoogleFonts(www.google.com/webfontsandtheexcellentLeagueofMovableType(www.theleagueofmoveabletype.com),soImustoverridethejQueryUICSSstylestoapplymypreferredfonttothebuttonelement.YoucanseetheresultofusingjQueryUItocreateabuttoninFigure2-6.Theresultis,asyoucansee,consistentwiththerestofthewebappbutmuchsimplertocreateinJavaScript.

Figure2-6.CreatingabuttonwithjQueryUI

ToolkitslikejQueryUIarejustaconvenientwrapperaroundthesameDOM,CSS,andeventtechniquesIdescribedearlier.Itisimportanttounderstandwhat’shappeningunderthecovers,butIrecommendusingjQueryUIoranothergoodUIlibrary.Theselibrariesarecomprehensivelytested,andtheysaveyoufromhavingtowriteanddebugcustomcode,allowingyoutospendmoretimeonthefeaturesthatsetyourwebappapartfromthecompetition.

Page 47: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER2GETTINGSTARTED

45

SummaryAsImentionedatthestartofthischapter,thetechniquesIusedintheseexamplesaresimple,reliable,andentirelysuitedtosmallwebapps.Thereisnothingintrinsicallywrongwithusingtheseapproachesiftheappissosmallthattherecanneverbeanyissueaboutmaintainingitbecauseeveryaspectofitsbehaviorisimmediatelyobvioustoaprogrammer.

However,ifyouarereadingthisbook,youwanttogofurtherandcreatewebappsthatarelarge,arecomplex,andhavemanymovingparts.Andwhenappliedtosuchwebapps,thesetechniquescreatesomefundamentalproblems.Theunderlyingissueisthatthedifferentaspectsofthewebappareallmixedtogether.Theapplicationdata(theproductsandthebasket),thepresentationofthatdata(theHTMLelements),andtheinteractionsbetweenthem(theJavaScripteventsandhandlerfunctions)aredistributedthroughoutthedocument.Thismakesithardtoaddadditionaldata,extendthefunctionality,orfixbugswithoutintroducingerrors.

Inthechaptersthatfollow,Ishowyouhowtoapplyheavy-dutytechniquesfromtheworldofserver-sidedevelopmenttothewebapp.Client-sidedevelopmenthasbeenthepoorcousinofserver-sideworkformanyyears,butasbrowsersbecomemorecapable(andaswebappprogrammersbecomemoreambitious),wecannolongerpretendthattheclientsideisanythingotherthanafull-fledgedplatforminitsownright.Itistimetotakewebappdevelopmentseriously,andinthechaptersthatfollow,Ishowyouhowtocreateasolid,robust,andscalablefoundationforyourwebapp.

Page 48: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

C H A P T E R 3

47

Adding a View Model

Ifyouhavedoneanyseriousdesktoporserver-sidedevelopment,youwillhaveencounteredeithertheModel-View-Controller(MVC)designpatternoritsderivativeModel-View-View-Model(MVVM).Iamnotgoingtodescribeeitherpatterninanydetail,otherthantosaythatthecoreconceptinbothisseparatingthedata,operations,andpresentationofanapplicationintoseparatecomponents.

Thereisalotofbenefitinapplyingthesamebasicprinciplestoawebapplication.Iamnotgoingtogetboggeddowninthedesignpatternsandterminology.Instead,Iamgoingtofocusondemonstratingtheprocessforstructuringawebappandexplainingthebenefitsthataregainedfromdoingso.

ResettingtheExampleThebestwaytounderstandhowtoapplyaviewmodelandthebenefitsthatdoingsoconfersistosimplydoit.ThefirstthingtodoiscuteverythingbutthebasicsoutoftheapplicationsothatIhaveacleanslatetostartfrom.AsyoucanseeinListing3-1,Ihaveremovedeverythingbutthebasicstructureofthedocument.

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>

Page 49: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER3ADDINGAVIEWMODEL

48

</div> <form action="/shipping" method="post"> <div id="buttonDiv"> <input type="submit" /> </div> </form> </body> </html>

CreatingaViewModelThenextstepistodefinesomedata,whichwillbethefoundationoftheviewmodel.Togetstarted,Ihaveaddedanobjectthatdescribestheproductsinthecheeseshop,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>

IhavecreatedanobjectthatcontainsdetailsofthecheeseproductsandassignedittoavariablecalledcheeseModel.TheobjectdescribesthesameproductsthatIusedChapter2andisthefoundationofmyviewmodel,whichIwillbuildthroughoutthechapter;itisasimpledataobjectnow,butI’llbedoingalotmorewithitsoon.

TipIfyoufindyourselfstaringattheblinkingcursorwithnorealideahowtodefineyourapplicationdata,thenmyadviceissimple:juststarttyping.Oneofthebiggestbenefitsofembracingaviewmodelisthatitmakeschangeseasier,andthatincludeschangestothestructureoftheunderlyingdata.Don’tworryifyoudon’tgetitright,becauseyoucanalwayscorrectitlater.

Page 50: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER3ADDINGAVIEWMODEL

49

AdoptingaViewModelLibraryFollowingtheprincipleofnotwritingwhatisavailableinagoodJavaScriptlibrary,Iwillintroduceaviewmodelintothewebappusingaviewmodellibrary.TheoneI’llbeusingiscalledKnockout(KO).IliketheKOapproachtoapplicationstructure,andthemainprogrammerforKOisSteveSanderson,whoismycoauthorfortheProASP.NETMVCbookfromApressandanall-aroundniceguy.TogetKO,gotohttp://knockoutjs.comandclicktheDownloadlink.Selectthemostrecentversion(whichis2.0.0asIwritethis)fromthelistoffilesandcopyittotheNode.jscontentdirectory.

TipDon’tworryifyoudon’tgetonwithKO.Otherstructurelibrariesareavailable.ThemaincompetitioncomesfromBackbone(http://documentcloud.github.com/backbone)andAngularJS(http://angularjs.org).Theimplementationdetailsinthesealternativelibrariesmaydiffer,buttheunderlyingprinciplesremainthesame.

Inthesectionsthatfollow,Iwillbringmyviewmodelandtheviewmodellibrarytogethertodecouplepartsoftheexampleapplication.

GeneratingContentfromtheViewModelTobegin,IamgoingtousethedatatogenerateelementsinthedocumentsothatIcandisplaytheproductstotheuser.Thisisasimpleuseoftheviewmodel,butitreproducesthebasicfunctionalityoftheimplementationinChapter2andgivesmeagoodfoundationfortherestofthechapter.Listing3-3showstheadditionoftheKOlibrarytothedocumentandthegenerationoftheelementsfromthedata.

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() {

www.allitebooks.com

Page 51: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER3ADDINGAVIEWMODEL

50

$('#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.ThefirstisimportingtheKOJavaScriptlibraryintothedocumentwithascriptelement.ThesecondadditiontellsKOtousemyviewmodelobject:

ko.applyBindings(cheeseModel);

ThekoobjectisthegatewaytotheKOlibraryfunctionality,andtheapplyBindingsmethodtakestheviewmodelobjectasanargumentandusesit,asthenamesuggests,tofulfillthebindingsdefinedinthedocument;thesearethethirdsetofadditions.YoucanseetheresultofthesebindingsinFigure3-1,andIexplainhowtheyworkinthesectionsthatfollow.

Page 52: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER3ADDINGAVIEWMODEL

51

Figure3-1.Creatingcontentfromtheviewmodel

UnderstandingValueBindingsAvaluebindingisarelationshipbetweenapropertyintheviewmodelandanHTMLelement.Thisisthesimplestkindofbindingavailable.HereisanexampleofanHTMLelementthathasavaluebinding:

<div class="grouptitle" data-bind="text: category"></div>

AllKObindingsaredefinedusingthedata-bindattribute.Thisisanexampleofatextbinding,whichhastheeffectofsettingthetextcontentoftheHTMLelementtothevalueofthespecifiedviewmodelproperty,inthiscase,thecategoryproperty.

WhentheapplyBindingsmethodiscalled,KOsearchesforbindingsandinsertstheappropriatedatavalueintothedocument,transformingtheelementlikethis:

<div class="grouptitle" data-bind="text: category">French Cheese</div>

TipIlikehavingtheKOdatabindingsdefinedintheelementswheretheywillbeapplied,butsomepeopledon’tlikethisapproach.ThereisasimplelibraryavailablethatsupportsunobtrusiveKOdatabindings,meaningthatthebindingsaresetupusingjQueryinthescriptelement.Youcangetthecodeandseeanexampleathttps://gist.github.com/1006808.

Page 53: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER3ADDINGAVIEWMODEL

52

TheotherbindingIusedinthisexamplewasattr,whichsetsthevalueofanelementattributetoapropertyfromthemodel.Hereisanexampleofanattrbindingfromthelisting:

<input data-bind="attr: {name: id}" value="0"/>

ThisbindingspecifiesthatKOshouldinsertthevalueoftheidpropertyforthenameattribute,whichproducesthefollowingresultwhenthebindingsareapplied:

<input data-bind="attr: {name: id}" value="0" name="camembert">

KOvaluebindingsdon’tsupportanyformattingorcombiningofvalues.Infact,valuebindingsjustinsertasinglevalueintothedocument,andthatmeansthatextraelementsareoftenneededastargetsforvaluebindings.Youcanseethisinthelabelelementinthelisting,whereIaddedacoupleofspanelements:

<label data-bind="attr: {for: id}" class="cheesename"> <span data-bind="text: name"></span> $(<span data-bind="text:price"></span>) </label>

Iwantedtoinserttwodatavaluesasthecontentforthelabelelementwithsomesurroundingcharacterstoindicatecurrency.Thewaytogetthedesiredeffectissimpleenough,albeititaddssomecomplexitytotheHTMLstructure.Analternativeistocreatecustombindings,whichIexplaininChapter4.

TipThetextandattrbindingsarethemostuseful,butKOsupportsotherkindsofvaluebindingsaswell:visible,html,css,andstyle.IusethevisiblebindinglaterinthechapterandthecssbindinginChapter4,butyoushouldconsulttheKOdocumentationatknockoutjs.comfordetailsoftheothers.

UnderstandingFlowControlBindingsFlowcontrolbindingsprovidethemeanstousetheviewmodeltocontrolwhichelementsareincludedinthedocument.Inthelisting,Iusedtheforeachbindingtoenumeratetheitemsviewmodelproperty.Theforeachbindingisusedonviewmodelpropertiesthatarearraysandduplicatesthesetofchildelementsforeachiteminthearray:

<div data-bind="foreach: items"> ... </div>

Valuebindingsonthechildelementscanrefertothepropertiesoftheindividualarrayitems,whichishowIamabletospecifytheidpropertyfortheattrbindingontheinputelement:KOknowswhicharrayitemisbeingprocessedandinsertstheappropriatevaluefromthatitem.

Page 54: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER3ADDINGAVIEWMODEL

53

TipInadditiontotheforeachbinding,KOalsosupportstheif,ifnot,andwithbindings,whichallowcontenttobeselectivelyincludedinorexcludedfromadocument.Idescribetheifandifnotbindingslaterinthischapter,butyoushouldconsulttheKOdocumentationatknockoutjs.comforfulldetails.

TakingAdvantageoftheViewModelNowthatIhavethebasicstructureoftheapplicationinplace,IcanusetheviewmodelandKOtodomore.Iwillstartwithsomebasicfeatureandthenstepthingsuptoshowyousomemoreadvancedtechniques.

AddingMoreProductstotheViewModelThefirstbenefitthataviewmodelbringsistheabilitytomakechangesmorequicklyandwithfewererrorsthanwouldotherwisebepossible.Thesimplestdemonstrationofthisistoaddmoreproductstothecheeseshopcatalog.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},

Page 55: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER3ADDINGAVIEWMODEL

54

{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.Ichangedthestructureofthedataobjectsothateachcategoryofproductsisanelementinanarrayassignedtotheproductsproperty(and,ofcourse,Iaddedtwonewcategories).IntermsoftheHTMLcontent,Ijusthadtoaddaforeachflowcontrolbindingsothattheelementscontainedwithinareduplicatedforeachcategory.

Page 56: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER3ADDINGAVIEWMODEL

55

TipTheresultoftheseadditionsisalong,thinHTMLdocument.Thisisnotanidealwayofdisplayingdata,butasIsaidinChapter1,thisisabookaboutadvancedprogrammingandnotabookaboutdesign.Therearelotsofwaystopresentthisdatamoreusefully,andIsuggeststartingbylookingatthetabswidgetsofferedbyUItoolkitssuchasjQueryUIorjQueryTools.

CreatingObservableDataItemsInthepreviousexample,IusedKOlikeasimpletemplateengine;Itookthevaluesfromtheviewmodelandusedthemtogenerateasetofelements.Ilikeusingtemplateenginesbecausetheysimplifymarkupandreduceerrors.Butabiggerbenefitofviewmodelscomeswhenyoucreateobservabledataitems.Putsimply,anobservabledataitemisapropertyintheviewmodelthat,whenupdated,causesalloftheHTMLelementsthathavevaluebindingstothatpropertytoupdateaswell.Listing3-5showshowtocreateanduseanobservabledataitem.

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}]}] };

Page 57: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER3ADDINGAVIEWMODEL

56

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>

Page 58: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER3ADDINGAVIEWMODEL

57

<div id="buttonDiv"> <input type="submit" /> </div> </form> </body> </html>

ThemapProductsfunctionisasimpleutilitythatallowsmetoapplyafunctiontoeachindividualcheeseproduct.ThisfunctionusesthejQueryeachmethod,whichexecutesafunctionforeveryiteminanarray.Byusingtheeachfunctiontwice,Icanreachtheinnerarrayofcheeseproductsineachcategory.

Inthisexample,Ihavetransformedthepricepropertyforeachcheeseproductintoanobservabledataitem,asfollows:

mapProducts(function(item) { item.price = ko.observable(item.price); });

Theko.observablemethodtakestheinitialvalueforthedataitemasitsargumentandsetsuptheplumbingthatisrequiredtodisseminateupdatestothebindingsinthedocument.Idon’thavetomakeanychangestothebindingsthemselves;KOtakescareofallthedetailsforme.

Allthatremainsistosetupasituationthatwillcauseachangetooccur.Ihavedonethisbyaddinganewbuttontothedocumentanddefiningahandlerfortheclickeventasfollows:

$('#discount').click(function() { mapProducts(function(item) { item.price(item.price() - 2); }); });

Whenthebuttonisclicked,IusethemapProductsfunctiontochangethevalueofthepricepropertyforeachcheeseobjectintheviewmodel.Sincethisisanobservabledataitem,thenewvaluewillbepushedouttothevaluebindingsandcausethedocumenttobeupdated.

NoticetheslightlyoddsyntaxIusewhenalteringthevalue.TheoriginalpricepropertywasaJavaScriptNumber,whichmeantIcouldchangethevaluelikethis:

item.price -= 2;

Buttheko.observablemethodtransformsthepropertyintoaJavaScriptfunctioninordertoworkwithsomeolderversionsofInternetExplorer.Thismeansyoureadthevalueofanobservabledataitembycallingthefunction(inotherwords,bycallingitem.price())andupdatethevaluebypassinganargumenttothefunction(inotherwords,bycallingitem.price(newValue)).Thiscantakealittlewhiletogetusedto,andIstillforgettodothis.

Figure3-2showstheeffectoftheobservabledataitem.WhentheApplyDiscountbuttonisclicked,allofthepricesdisplayedtotheuserareupdated,asFigure3-2shows.

Page 59: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER3ADDINGAVIEWMODEL

58

Figure3-2.Usinganobservabledataitem

Thepowerandflexibilityofanobservabledataitemissignificant;itcreatesanapplicationwherechangesfromtheviewmode,irrespectiveofhowtheyarise,causethedatabindingsinthedocumenttobeupdatedimmediately.Asyou’llseeintherestofthechapter,ImakealotofuseofobservabledataitemsasIaddmorecomplexfeaturestotheexamplewebapp.

CreatingBidirectionalBindingsAbidirectionalbindingisatwo-wayrelationshipbetweenaformelementandanobservabledataitem.Whentheviewmodelisupdated,soisthevalueshownintheelement,justasforaregularobservable.Inaddition,changingtheelementvaluecausesanupdatetogointheotherdirection:thepropertyintheviewmodelisupdated.So,forexample,ifIuseabidirectionalbindingforaninputelement,KOensuresthatthemodelisupdatedwhentheuserentersanewvalue.Byusingbidirectionalrelationshipsbetweenmultipleelementsandthesamemodelproperty,youcaneasilykeepacomplexwebappsynchronizedandconsistent.

Todemonstrateabidirectionalbinding,IwilladdaSpecialOfferssectiontothecheeseshop.Thisallowsmetopicksomeproductsfromthefullsection,applyadiscount,and,ideally,drawthecustomer’sattentiontoaproductthattheymightnototherwiseconsider.

Listing3-6containsthechangestothewebapptosupportthespecialoffers.Tosetupabidirectionalbinding,Iamgoingtodotwootherinterestingthings:extendtheviewmodelanduseKOtemplatestogenerateelements.I’llexplainallthreechangesinthesectionsthatfollowthelisting.

Page 60: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER3ADDINGAVIEWMODEL

59

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) {

www.allitebooks.com

Page 61: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER3ADDINGAVIEWMODEL

60

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>

ExtendingtheViewModelJavaScript’sloosetypinganddynamicnaturemakesitidealforcreatingflexibleandadaptableviewmodels.Ilikebeingabletotaketheinitialdataandreshapeittocreatesomethingthatismorecloselytailoredtotheneedsofthewebapp,inthiscase,toaddsupportforspecialoffers.Tostartwith,Iaddapropertycalledspecialstotheviewmodel,definingitasanobjectthathascategoryanditemspropertiesliketherestofthemodelbutwithsomeusefuladditions:

Page 62: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER3ADDINGAVIEWMODEL

61

cheeseModel.specials = { category: "Special Offers", discount: 3, ids: ["stilton", "tomme"], items: [] };

ThediscountpropertyspecifiesthedollardiscountIwanttoapplytothespecialoffers,andtheidspropertycontainsanarrayoftheIDsofproductsthatwillbespecialoffers.

Thespecials.itemsarrayisemptywhenIfirstdefineit.Topopulatethearray,Ienumeratetheproductsarraytofindthoseproductsthatareinthespecials.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); });

IusetheinArraymethodtodeterminewhetherthecurrentitemintheiterationisoneofthosethatwillbeincludedasaspecialoffer.TheinArraymethodisanotherjQueryutility,anditreturnstheindexofanitemifitiscontainedwithinanarrayand-1ifitisnot.ThisisaquickandeasywayformetochecktoseewhetherthecurrentitemisonethatIaminterestedinasaspecialoffer.

Ifanitemisonthespecialslist,thenIreducethevalueofthepricepropertybythediscountamountandusethepushmethodtoinserttheitemintothespecials.itemsarray.

item.price -= cheeseModel.specials.discount; cheeseModel.specials.items.push(item);

AfterIhaveiteratedthroughtheitemsintheviewmodel,thespecials.itemarraycontainsacompletesetoftheproductsthataretobediscounted,and,alongtheway,Ihavereducedeachoftheirprices.

Inthisexample,Ihavemadethequantitypropertyintoanobservabledataitem:

item.quantity = ko.observable(0);

ThisisimportantbecauseIamgoingtodisplaymultipleinputelementsforthespecialoffers:oneelementintheoriginalcheesecategoryandanotherinanewSpecial OfferscategorythatIexplaininthenextsection.Byusinganobservabledataitemandbidirectionalbindingsontheinputelements,Icaneasilymakesurethatthequantitiesenteredforacheeseareconsistentlydisplayed,irrespectiveofwhichinputelementisused.

GeneratingtheContentAllthatremainsnowistogeneratethecontentfromtheviewmodel.Iwanttogeneratethesamesetofelementsforthespecialoffersasfortheregularcategories,soIhaveusedtheKOtemplatefeature,whichallowsmetogeneratethesamesetofelementsatmultiplepointsinthedocument.Hereisthetemplatefromthelisting:

<script id="categoryTmpl" type="text/html"> <div class="cheesegroup"> <div class="grouptitle" data-bind="text: category"></div>

Page 63: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER3ADDINGAVIEWMODEL

62

<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,whichpreventsthebrowserfromexecutingthecontentasJavaScript.MostofthebindingsinthetemplatearethesametextandattrbindingsIusedinthepreviousexample.Theimportantadditionistotheinputelement,asfollows:

<input data-bind="attr: {name: id}, value: quantity"/>

Thedata-bindattributeforthiselementdefinestwobindings,separatedbyacomma.Thefirstisaregularattrbinding,butthesecondisavaluebinding,whichisoneofthebidirectionalbindingsthatKOdefines.Idon’thavetotakeanyactiontomakethevaluebindingbidirectional;KOtakescareofitautomatically.Inthislisting,Icreateatwo-waybindingtothequantityobservabledataitem.

Igeneratecontentfromthetemplateusingthetemplatebinding.Whenusingatemplate,KOduplicatestheelementsthatitcontainsandinsertsthemaschildrenoftheelementthathasthetemplatebinding.TherearetwopointsinthedocumentwhereIusethetemplate,andtheyareslightlydifferent:

<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,thenamepropertyspecifiestheidattributevalueofthetemplateelement.Ifyouwanttogenerateonlyonesetofelements,thenyoucanusethedatapropertytospecifywhichviewmodelpropertywillbeused.Iuseddatatospecifythespecialspropertyinthelisting,whichcreatesasectionofcontentformyspecial-offerproducts.

TipYoumustremembertoenclosetheidofthetemplateelementinquotes.Ifyoudon’t,KOwillfailquietlywithoutgeneratingelementsfromthetemplate.

Youcanusetheforeachpropertyifyouwanttogenerateasetofelementsforeachiteminanarray.Ihavedonethisfortheregularproductcategoriesbyspecifyingtheproductsarray.Inthisway,Icanapplythetemplatetoeachelementinanarraytogeneratecontentconsistently.

Page 64: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER3ADDINGAVIEWMODEL

63

TipNoticethatthespecial-offerelementsareinsertedoutsidetheformelement.Theinputelementsforthespecial-offerproductswillhavethesamenameattributevalueasthecorrespondinginputelementintheregularproductcategory.Byinsertingthespecial-offerelementsoutsidetheform,Ipreventduplicateentriesfrombeingsenttotheserverwhentheformissubmitted.

ReviewingtheResultNowthatIhaveexplainedeachofthechangesImadetosetupthebidirectionalbindings,itistimetolookattheresults,whichyoucanseeinFigure3-3.

Figure3-3.Theresultofextendingtheviewmodel,creatingalivebinding,andusingtemplates

Thisisgooddemonstrationofhowusingaviewmodelcansavetimeandreduceerrors.Ihaveapplieda$3discounttotheSpecialOfferproducts,whichIdidbyalteringthevalueofthepricepropertyintheviewmodel.Eventhoughthepricepropertyisnotobservable,thecombinationoftheviewmodelandthetemplateensuresthatthecorrectpricesaredisplayedthroughoutthedocumentwhentheelementsareinitiallygenerated.(YoucanseethatbothStiltonlistingsarepricedat$6,ratherthanthe$9originallyspecifiedbytheviewmodel.)

Thebidirectionalbindingisthemostinterestingandusefulfeatureinthisexample.Alloftheinputelementshavebidirectionalbindingswiththeircorrespondingquantityproperty,andsincetherearetwoinputelementsinthedocumentforeachoftheSpecialOffercheeses,enteringavalueintoonewill

Page 65: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER3ADDINGAVIEWMODEL

64

immediatelycausethatvaluetobedisplayedintheother;youcanseethishashappenedfortheStiltonproductinthefigure(butitisaneffectthatisbestexperiencedbyloadingtheexampleinthebrowser).

So,withverylittleeffort,Ihavebeenabletoenhancetheviewmodelandusethoseenhancementstokeepaformconsistentandresponsive,whileaddingnewfeaturestotheapplication.Inthenextsection,I’llbuildontheseenhancementstocreateadynamicbasket,showingyousomeoftheotherbenefitsthatcanarisefromaviewmodel.

TipIfyousubmitthisformtotheserver,theordersummarywillshowtheoriginal,undiscountedprice.Thisis,ofcourse,becauseIappliedthediscountonlyinthebrowser.Inarealapplication,theserverwouldalsoneedtoknowaboutthespecialoffers,butIamgoingtoskipoverthis,sincethisbookfocusesonclient-sidedevelopment.

AddingaDynamicBasketNowthatIhaveexplainedanddemonstratedhowchangesaredetectedandpropagatedwithvalueandbidirectionalbindings,IcancompletetheexamplesothatallofthefunctionalitypresentinChapter2isavailabletotheuser.ThismeansIneedtoimplementadynamicshoppingbasket,whichIdointhesectionsthatfollow.

AddingSubtotalsWithaviewmodel,newfeaturescanbeaddedquickly.Thechangestoaddper-itemsubtotalsaresurprisinglysimple,althoughIneedtousesomeadditionalKOfeatures.First,Ineedtoenhancetheviewmodel.Listing3-7highlightsthechangesinthescriptelementwithinthecalltothemapProductfunction.

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.Thisislikearegularobservableitem,exceptthatthevalueisproducedbyafunction,whichispassedasthefirstargumenttotheko.computedmethod.Thesecondmethodisusedasthevalueofthethisvariablewhenthefunctionisexecuted;Ihavesetthistotheitemloopvariable.

Page 66: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER3ADDINGAVIEWMODEL

65

ThenicethingaboutthisfeatureisthatKOmanagesallofthedependencies,suchthatwhenmycomputedobservablefunctionreliesonaregularobservabledataitem,achangetotheregularitemautomaticallytriggersanupdateinthecomputedvalue.I’llusethisbehaviortomanagetheoveralltotallaterinthischapter.

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>

TheinnerspanelementusesatextdatabindingtodisplaythevalueofthesubtotalpropertyIcreatedamomentago.Tomakethingsmoreinteresting,theouterspanelementusesanotherKObinding;thisoneisvisible.Forthisbinding,thechildelementsarehiddenwhenthespecifiedpropertyisfalse-like(zero,null,undefined,orfalse).Fortruth-likevalues(1,true,oranon-nullobjectorarray),thechildelementsaredisplayed.Ihavespecifiedthesubtotalvalueforthevisiblebinding,andthislittletrickmeansthatIwilldisplayasubtotalonlywhentheuserentersanonzerovalueintotheinputelement.YoucanseetheresultinFigure3-4.

Figure3-4.Selectivelydisplayingsubtotals

Page 67: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER3ADDINGAVIEWMODEL

66

Youcanseehoweasyandquickitistocreatenewfeaturesoncethebasicstructurehasbeenaddedtotheapplication.Somenewmarkupandalittlescriptgoalongway.And,asabonus,thesubtotalfeatureworksseamlesslywiththespecialoffers;sincebothoperateontheviewmodel,thediscountsappliedforthespecialoffersareseamlessly(andeffortlessly)incorporatedintothesubtotals.

AddingtheBasketLineItemsandTotalIdon’twanttousetheinlinebasketapproachthatItookinChapter2becausesomeoftheproductsareshowntwiceandthedocumentistoolongtomaketheuserscrolldowntoseethetotalcostoftheirselection.Instead,Iamgoingtocreateaseparatesetofbasketelementsthatwillbedisplayedalongsidetheproducts.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>

Page 68: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER3ADDINGAVIEWMODEL

67

<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;

Page 69: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER3ADDINGAVIEWMODEL

68

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>

Page 70: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER3ADDINGAVIEWMODEL

69

</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,pleasereflectonhowlittlehastochangetoaddthisfeature.Onceagain,aviewmodelandsomebasicapplicationstructurecreateafoundationtowhichnewfeaturescanbequicklyandeasilyadded.

ExtendingtheViewModelThechangetotheviewmodelinthislistingistheadditionofthetotalproperty,whichisacomputedobservablethatsumstheindividualsubtotalvalues:

cheeseModel.total = ko.computed(function() { var total = 0; mapProducts(function(elem) { total += elem.subtotal(); }); return total; });

www.allitebooks.com

Page 71: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER3ADDINGAVIEWMODEL

70

AsImentionedpreviously,KOtracksdependenciesbetweenobservabledataitemsautomatically.Anychangetoasubtotalvaluewillcausetotaltoberecalculatedandthenewvaluetobedisplayedinelementsthatareboundtoit.

AddingtheBasketStructureandTemplateTheouterstructureoftheHTMLelementsIaddedtothedocumentisjustaduplicateofacheesecategorytomaintainvisualconsistency.Theheartofthebasketisthetableelement,whichcontainsseveraldatabindings:

<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.Thisisknownasacontainerlessbinding,anditallowsmetoapplythetemplate bindingwithoutneedingacontainerelementforthecontentthatwillbeduplicated.AddingrowstoatablefromanestedarrayisaperfectsituationforthistechniquebecauseaddinganelementjustsoIcanapplythebindingwouldcauselayoutproblems.Thecontainerlessbindingiscontainedwithinaregularforeachbinding,butyoucannestthebindingcommentsmuchasyouwouldregularelements.

Theotherbindingisasimpletextvaluebinding,whichdisplaystheoveralltotalforthebasket,usingthecalculatedtotalobservableIcreatedamomentago.Idon’thavetotakeanyactiontomakesurethatthetotalisup-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.Thevisiblebindingonthetrelementensuresthattablerowsarevisibleonlyforthosecheesesforwhichthequantityisn’tzero;thispreventsthebasketfrombeingfilledupwithrowsforproductsthattheuserisn’tinterestedin.

Notetheattrbindingonthetrelement.IhavedefinedacustomattributeusingtheHTML5dataattributefeaturethatembedstheidvalueoftheproductthattherowrepresentsintothetrelement.I’llexplainwhyIdidthisshortly.

Page 72: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER3ADDINGAVIEWMODEL

71

Ialsomovedthesubmitbuttonsothatitisunderthebasket,makingiteasierfortheusertosubmittheirorder.ThestylethatIassignedtothebasketelementsusesthefixedvaluefortheCSSpositionproperty,meaningthatthebasketwillalwaysbevisible,evenastheuserscrollsdownthepage.Toaccommodatethebasket,IusedjQuerytoapplyanewvaluefortheCSSwidthpropertydirectlytothecheesecategoryelements(butnotthebasketitself):

$('div.cheesegroup').not("#basket").css("width", "50%");

RemovingItemsfromtheBasketThelastsetofchangesbuildsontheaelementsthatareaddedtoeachtablerowinthebasketRowTmpltemplate:

$('#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.jQueryUIthemesincludeasetoficons,andtheobjectthatIpasstothejQueryUIbuttonmethodcreatesabuttonthatusesoneoftheseimagesanddisplaysnotext.Thisgivesmeanicesmallbuttonwithacross.

Intheclickfunction,IusejQuerytonavigatefromtheaelementthattriggeredtheclickeventtothefirstancestortrelementusingtheclosestmethod.ThisselectsthetrelementthatcontainsthecustomdataattributeIinsertedinthetemplateearlierandthatIreadusingtheattrmethod:

var targetId = $(this).closest('tr').attr("data-prodId");

Thisstatementletsmedeterminetheidoftheproducttheuserwantstoremovefromthebasket.IthenusethemapProductsfunctiontofindthematchingcheeseobjectandsetthequantitytozero.Sincequantityisanobservabledataitem,KOdisseminatesthenewvalue,whichcausesthesubtotalvaluetoberecalculatedandthevisiblebindingonthecorrespondingtrelementtobereevaluated.Sincethequantityiszero,thetablerowwillbehiddenautomatically.And,sincesubtotalisobservable,thetotalwillalsoberecalculated,andthenewvalueisdisplayedtotheuser.Asyoucansee,itisusefultohaveaviewmodelwherethedependenciesbetweendatavaluesaremanagedseamlessly.Thenetresultisadynamicbasketthatisalwaysconsistentwiththevaluesintheviewmodelandsoalwayspresentsthecorrectinformationtotheuser.

FinishingtheExampleBeforeIfinishthistopic,Ijustwanttotweakacoupleofthings.First,thebasketlooksprettypoorwhennoitemshavebeenselectedbytheuser,asshowninFigure3-6.Toaddressthis,Iwilldisplaysomeplaceholdertextwhenthebasketisempty.

Page 73: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER3ADDINGAVIEWMODEL

72

Figure3-6.Theemptybasket

Second,theuserhasnowaytoclearthebasketwithasingleaction,soIwilladdabuttonthatwillresetthequantitiesofalloftheproductstozero.Finally,bymovingthesubmitbuttonoutsidetheformelement,Ihavelosttheabilitytorelyonthedefaultaction.Imustaddaneventhandlersothattheusercansubmittheform.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>

Page 74: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER3ADDINGAVIEWMODEL

73

</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.KOdefinesapairofbindings,ifandifnot,thataresimilartothevisiblebindingbutthataddandremoveelementstotheDOM,ratherthansimplyhidingthemfromview.Theifbindingshowsitselementswhenthespecifiedviewmodelpropertyistrue-likeandhidesthemifitisfalse-like.Theifnotbindingisinverted;itshowsitselementswhenthepropertyistrue-like.

Byspecifyingtheifnotbindingwiththetotalproperty,Iensurethatmyplaceholderelementisshownonlywhentotaliszero,whichhappenswhenallofthesubtotalvaluesarezero,whichhappenswhenallofthequantityvaluesarezero.Onceagain,IamrelyingonKO’sabilitytomanagethedependenciesbetweenobservabledataitemstogettheeffectIrequire.

Iwantthetableelementtobeinvisiblewhentheplaceholderisshowing,soIhaveusedthevisiblebinding.

Icouldhaveusedtheifbinding,butdoingsowouldhavecausedaproblem.Thebindingtothetotalpropertymeansthatthetablewillnotbeshowninitially,andwiththeifbinding,theelementwouldhavebeenremovedfromtheDOM.ThismeansthattheaelementswouldalsonotbepresentwhenItrytoselectthemtosetuptheremovebuttons.ThevisiblebindingleavestheelementsinthedocumentforjQuerytofindbuthidesthemfromtheuser.

YoumightwonderwhyIdon’tmovethejQueryselectionsothatitisperformedbeforethecalltoko.applyBindings.ThereasonisthattheaelementsIwanttoselectwithjQueryarecontainedintheKOtemplate,whichisn’tusedtocreateelementsuntiltheapplyBindingsmethodiscalled.Thereisnogoodwayaroundthis,andsothevisiblebindingisrequired.

TheonlyotherchangetotheHTMLelementsistheadditionofaninputelementwhosetypeisreset.Thiselementisoutsideoftheformelement,soIwillhavetohandletheclickeventtoremoveitemsfromthebasket.Listing3-11showsthecorrespondingchangestothescriptelement.

Listing3-11.EnhancingtheScripttoFinishtheExample

... <script> // ...code removed for brevity... // $(document).ready(function() { $('#buttonDiv input').button().css("font-family", "Yanone") .click(function() {

Page 75: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER3ADDINGAVIEWMODEL

74

if (this.type == "submit") { $('form').submit(); } else if (this.type == "reset") { mapProducts(function(item) { item.quantity(0); }) } }); // ...code removed for brevity... // }); </script>

Ihaveshownonlypartofthescriptinthelistingbecausethechangesarequiteminor.NoticehowIamabletousejQueryandplainJavaScripttomanipulatetheviewmodel.Idon’tneedtoaddanycodeforthebasketplaceholder,sinceitwillbemanagedbyKO.Infact,allIneeddoiswidenthejQueryselectionsothatIcreatejQueryUIbuttonwidgetsforboththesubmitandresetinputelementsandaddaclickhandlerfunction.InthefunctionIsubmittheformorchangethequantityvaluestozerodependingonwhichbuttontheuserclicks.YoucanseetheplaceholderforthebasketinFigure3-7.

Figure3-7.Usingaplaceholderwhenthebasketisempty

Youwillhavetoloadtheexamplesinabrowserifyouwanttoseehowthebuttonswork.TheeasiestwaytodothisistousethesourcecodedownloadthataccompaniesthisbookandthatisavailablewithoutchargeatApress.com.

SummaryInthischapter,Ishowedyouhowtoembracethekindofdesignphilosophythatyoumayhavepreviouslyusedindesktoporserver-sidedevelopment,oratleastasmuchofthatphilosophyasmakessenseforyourproject.

Byaddingaviewmodeltomywebapp,Iwasabletocreateamuchmoredynamicversionoftheexampleapplication;it’sonethatismorescalable,easiertotestandmaintain,andmakeschangesandenhancementabreeze.

YoumayhavenoticedthattheshapeofastructuredwebapplicationchangessothatthereisalotmorecoderelativetotheamountofHTMLmarkup.Thisisagoodthing,becauseitputsthecomplexity

Page 76: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER3ADDINGAVIEWMODEL

75

oftheapplicationwhereyoucanbetterunderstand,test,andmodifyit.TheHTMLbecomesaseriesofviewsortemplatesforyourdata,drivenfromtheviewmodelviathestructurelibrary.Icannotemphasizethebenefitsofembracingthisapproachenough;itreallydoessetthefoundationforprofessional-levelwebappsandwillmakecreating,enhancing,andmaintainingyourprojectssimpler,easier,andmoreenjoyable.

Page 77: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

C H A P T E R 4

77

Using URL Routing

Inthischapter,Iwillshowyouhowtoaddanotherserver-sideconcepttoyourwebapp:URLrouting.TheideabehindURLroutingisverysimple:weassociateJavaScriptfunctionswithinternalURLs.AninternalURLisonethatisrelativetothecurrentdocumentandcontainsahashfragment.Infact,theyareusuallyexpressedasjustthehashfragmentonitsown,suchas#summary.

Undernormalcircumstances,whentheuserclicksalinkthatpointstoaninternalURL,thebrowserwillseewhetherthereisanelementinthedocumentthathasanidattributevaluethatmatchesthefragmentand,ifthereis,scrolltomakethatelementvisible.

WhenweuseURLrouting,werespondtothesenavigationchangesbyexecutingJavaScriptfunctions.Thesefunctionscanshowandhideelements,changetheviewmodel,orperformothertasksyoumightneedinyourapplication.Usingthisapproach,wecanprovidetheuserwithamechanismtonavigatethroughourapplication.

Wecould,ofcourse,useevents.Theproblemis,onceagain,scale.Handlingeventstriggeredbyelementsisaperfectlyworkableandacceptableapproachforsmallandsimplewebapplications.Forlargerandmorecomplexapps,weneedsomethingbetter,andURLroutingprovidesaniceapproachthatissimple,iselegant,andscaleswell.Addingnewfunctionalareastothewebapp,andprovidinguserswiththemeanstousethem,becomesincrediblysimpleandrobustwhenweuseURLsasthenavigationmechanism.

BuildingaSimpleRoutedWebApplicationThebestwaytoexplainURLroutingiswithasimpleexample.Listing4-1showsabasicwebapplicationthatreliesonrouting.

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>

Page 78: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

78

<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’llbreakthingsdownandexplainthemovingpartsinthesectionsthatfollow.

AddingtheRoutingLibraryOnceagain,IamgoingtouseapublicallyavailablelibrarytogettheeffectIrequire.ThereareafewURLroutinglibrariesaround,buttheonethatIlikeiscalledCrossroads.Itissimple,reliable,andeasytouse.Ithasonedrawback,whichisthatitdependsontwootherlibrariesbythesameauthor.Iliketoseedependenciesrolledintoasinglelibrary,butthisisnotauniversallyheldpreference,anditjustmeansthatwehavetodownloadacoupleofextrafiles.Table4-1liststheprojectsandtheJavaScriptfilesthat

Page 79: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

79

werequirefromthedownloadarchives,whichshouldbecopiedintotheNode.jsservercontentdirectory.(Allthreefilesarepartofthesourcecodedownloadforthisbookifyoudon’twanttodownloadthesefilesindividually.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> ...

AddingtheViewModelandContentMarkupURLroutingworksextremelywellwhencombinedwithaviewmodelinawebapplication.Forthisinitialapplication,Ihavecreatedaverysimpleviewmodel,asfollows:

var viewModel = { items: ["Apple", "Orange", "Banana"], selectedItem: ko.observable("Apple") };

Therearetwopropertiesintheviewmodel.Theitemspropertyreferstoanarrayofthreestrings.TheselectedItempropertyisanobservabledataitemthatkeepstrackofwhichitemispresentlyselected.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,whichisexactlywhatIhavedoneforthefadeVisiblebindingreferredtointhelisting.Listing4-2showsthe

Page 80: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

80

definitionofthisbinding,whichIhaveplacedinafilecalledutils.js(whichyoucanseeimportedinascriptelementinListing4-1).Thereisnorequirementtouseanexternalfile;IhaveusedonebecauseIintendtoemploythisbindingagainwhenIaddroutingtotheCheeseLuxexamplelaterinthechapter.

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.Thevalueofthepropertyisanobjectwithtwomethods:initandupdate.Theinitmethodiscalledwhenko.applyBindingsiscalled,andtheupdatemethodiscalledwhenobservabledataitemsthatthebindingdependsonchange.

Theargumentstobothmethodsaretheelementtowhichthebindinghasbeenappliedtoandanaccessorobjectthatprovidesaccesstothebindingargument.Thebindingargumentiswhateverfollowsthebindingname:

data-bind="fadeVisible: $data == viewModel.selectedItem()"

Ihaveused$datainmybindingargument.Whenusingaforeachbinding,$datareferstothecurrentiteminthearray.IcheckthisvalueagainsttheselectedItemobservabledataitemintheviewmodel.Ihavetorefertotheobservablethroughtheglobalvariablebecauseitisnotwithinthecontextoftheforeachbinding,andthismeansIneedtotreattheobservablelikeafunctiontogetthevalue.WhenKOcallstheinitorupdatemethodofmycustombinding,theexpressioninthebindingargumentisresolved,andtheresultofcallingaccessor()istrue.

Inmycustombinding,theinitmethodusesjQuerytoshoworhidetheelementtowhichthebindinghasbeenappliedbasedontheaccessorvalue.ThismeansthatonlytheelementsthatcorrespondtotheselectedItemobservablearedisplayed.

Theupdatemethodworksdifferently.IusejQueryeffectstoanimatethetransitionfromonesetofelementstoanother.Iftheupdatemethodisbeingcalledfortheelementsthatshouldbedisplayed,IselecttheelementsthatarepresentlyvisibleandcallthefadeOutmethod.Thiscausestheelementstograduallybecometransparentandtheninvisible;oncethishashappened,IthenusefadeIntomaketherequiredelementsvisible.Theresultisasmoothtransitionfromonesetofelementstoanother.

Page 81: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

81

AddingtheNavigationMarkupIgenerateasetofaelementstoprovidetheuserwiththemeanstoselectdifferentitems;inmysimpleapplication,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.Mostofthetime,thiscanbeworkedaroundbyaddingspanordivelementstoprovidestructuretowhichbindingscanbeattached.Thisapproachdoesn’tworkwhenitcomestoattributevalues,whichisaproblemwhenusingURLrouting.WhatIwantisaseriesofaelementswhosehrefattributecontainsavaluefromtheviewmodel,likethis:

<a href="#/select/Apple">Apple</a>

Ican’tgettheresultIwantfromthestandardattrbinding,soIhavecreatedanothercustomone.Listing4-3showsthedefinitionoftheformatAttrbinding.I’llbeusingthisbindinglater,soIhavedefineditintheutil.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.ThebindingargumentIhaveusedontheelementisaJavaScriptobject,whichbecomesobviouswithsomejudiciousreformatting:

formatAttr: {attr: 'href', prefix: '#select/', value: $data }, css: {selectedItem: ($data == viewModel.selectedItem())}

KOresolvesthedatavaluesbeforepassingthisobjecttomyinitorupdatemethods,givingmesomethinglikethis:

Page 82: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

82

{attr: 'href', prefix: '#select/', value: Apple}

Iusethepropertiesofthisobjecttocreatetheformattedstring(usingthecomposeStringfunctionIdefinedalongsidethecustombinding)tocombinethecontentofvaluepropertywiththevalueoftheprefixandsuffixpropertiesiftheyaredefined.

Therearetwootherbindings.ThecssbindingappliesandremovesaCSSclass;IusethisbindingtoapplytheselectedItemclass.Thiscreatesasimpletogglebutton,showingtheuserwhichbuttonisclicked.Thetextbindingisappliedtoachildspanelement.ThisistoworkaroundaproblemwherejQueryUIandKObothassumecontroloverthecontentsoftheaelement;applyingthetextattributetoanestedelementavoidsthisconflict.IneedthisworkaroundbecauseIusejQueryUItocreatebuttonwidgetsfromthenavigationelements,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,Iamabletocreateasetofbuttonsfromthechildaelements.Ihaveusedbuttonset,ratherthanbutton,sothatjQueryUIwillstyletheelementsinacontiguousblock.YoucanseetheeffectthatthiscreatesinFigure4-1.

Figure4-1.Thebasicapplicationtowhichroutingisapplied

Thereisnospacebetweenbuttonscreatedbythebuttonsetmethod,andtheouteredgesofthesetarenicelyrounded.Youcanalsoseeoneofthecontentelementsinthefigure.Theideaisthatclickingoneofthebuttonswillallowtheusertodisplaythecorrespondingcontentitem.

f

Page 83: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

83

ApplyingURLRoutingIhavealmosteverythinginplace:asetofnavigationalcontrolsandasetofcontentelements.Inowneedtotiethemtogether,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>

ThefirstthreeofthehighlightedstatementssetuptheHasherlibrarysothatitworkswithCrossroads.HasherrespondstotheinternalURLchangethroughthelocation.hashbrowserobjectandnotifiesCrossroadswhenthereisachange.

CrossroadsexaminesthenewURLandcomparesittoeachoftheroutesithasbeengiven.RoutesaredefinedusingtheaddRoutemethod.ThefirstargumenttothismethodistheURLweareinterestedin,andthesecondargumentisafunctiontoexecuteiftheuserhasnavigatedtothatURL.So,forexample,iftheusernavigatesto#select/Apple,thenthefunctionthatsetstheselectedItemobservableintheviewmodeltoApplewillbeexecuted.

TipWedon’thavetospecifythe#characterwhenusingtheaddRoutemethodbecauseHasherremovesitbeforenotifyingCrossroadsofachange.

Intheexample,Ihavedefinedthreeroutes,eachofwhichcorrespondstooneoftheURLsthatIcreatedusingtheformatAttrbindingontheaelements.

Page 84: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

84

ThisisattheheartofURLrouting.YoucreateasetofURLroutesthatdrivethebehaviorofthewebappandthencreateelementsinthedocumentthatnavigatetothoseURLs.Figure4-2showstheeffectofsuchnavigationintheexample.

Figure4-2.Navigatingthroughtheexamplewebapp

Whentheuserclicksabutton,thebrowsernavigatestotheURLspecifiedbythehrefattributeoftheunderlyingaelement.Thisnavigationchangeisdetectedbytheroutingsystem,whichtriggersthefunctionthatcorrespondstotheURL.Thefunctionchangesthevalueofanobservableitemintheviewmodel,andthatcausestheelementsthatrepresenttheselecteditemtobedisplayedbytheuser.

Theimportantpointtounderstandisthatweareworkingwiththebrowser’snavigationmechanism.Whentheuserclicksoneofthenavigationelements,thebrowsermovestothetargetURL;althoughtheURLiswithinthesamedocument,thebrowser’shistoryandURLbarareupdated,asyoucanseeinthefigure.

Thisconferstwobenefitsonawebapplication.ThefirstisthattheBackbuttonworksthewaythatmostusersexpectittowork.ThesecondisthattheusercanenteraURLmanuallyandnavigatetoaspecificpartoftheapplication.Toseebothofthesebehaviorsinaction,followthesesteps:

1. Loadthelistinginthebrowser.

2. ClicktheOrangebutton.

3. Entercheeselux.com/#select/Bananaintothebrowser’sURLbar.

4. Clickthebrowser’sBackbutton.

WhenyouclickedtheOrangebutton,theOrangeitemwasselected,andthebuttonwashighlighted.SomethingsimilarhappensfortheBananaitemwhenyouenteredtheURL.Thisisbecausethenavigationmechanismfortheapplicationisnowmediatedbythebrowser,andthisishowweareabletouseURLroutingtodecoupleanotheraspectoftheapplication.

Thefirstbenefitis,tomymind,themostuseful.WhentheuserclickstheBackbutton,thebrowsernavigatesbacktothelastvisitedURL.Thisisanavigationchange,andifthepreviousURLiswithinourdocument,thenewURLismatchedagainstthesetofroutesdefinedbytheapplication.Thisisanopportunitytounwindtheapplicationstatetothepreviousstep,whichinthecaseofthesampleapplicationdisplaystheOrangebutton.Thisisamuchmorenaturalwayofworkingforauser,especiallycomparedtousingregularevents,whereclickingtheBackbuttontendstonavigatetothesitetheuservisitedbeforeourapplication.

Page 85: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

85

ConsolidatingRoutesInthepreviousexample,Idefinedeachrouteandthefunctionitexecutedseparately.Ifthisweretheonlywaytodefineroutes,acomplexwebappwouldendupwithamorassofroutesandfunctions,andtherewouldbenoadvantageoverregulareventhandling.Fortunately,URLsroutingisveryflexible,andwecanconsolidateourrouteswithease.Idescribethetechniquesavailableforthisinthesectionsthatfollow.

UsingVariableSegmentsListing4-4showshoweasyitistoconsolidatethethreeroutesfromtheearlierdemonstrationintoasingleroute.

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/Applehastwosegments,whichareselectandApple.WhenIspecifyaroute,likethis:

/select/Apple

theroutewillmatchaURLonlyifbothsegmentsmatchexactly.Inthelisting,Ihavebeenabletoconsolidatemyroutesbyaddingavariablesegment.AvariablesegmentallowsaroutetomatchaURLthathasanyvalueforthecorrespondingsegment.So,tobeclear,allofthenavigationURLsinthesimplewebappwillmatchmynewroute:

select/Apple select/Orange select/Banana

Thefirstsegmentisstillstatic,meaningthatonlyURLswhosefirstsegmentisselectwillmatch,butIhaveessentiallyaddedawildcardforthesecondsegment.

Page 86: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

86

SothatIcanrespondappropriatelytotheURL,thecontentofthevariablesegmentispassedtomyfunctionasanargument.IusethisargumenttochangethevalueoftheselectedItemobservableintheviewmodel,meaningthataURLof/select/Appleresultsinacalllikethis:

viewModel.selectedItem('Apple');

andaURLofselect/Cherrywillresultinacalllikethis:

viewModel.selectedItem('Cherry');

DealingwithUnexpectedSegmentValuesThatlastURLisaproblem.Thereisn’tanitemcalledCherryinmywebapp,andsettingtheviewmodelobservabletothisvaluewillcreateanoddeffectfortheuser,asshowninFigure4-3.

Figure4-3.Theresultofanunexpectedvariablesegmentvalue

TheflexibilitythatcomeswithURLroutingcanalsobeaproblem.Beingabletonavigatetoaspecificpartoftheapplicationisausefultoolfortheuser,but,aswithallopportunitiesfortheusertoprovideinput,wehavetoguardagainstunexpectedvalues.Formyexampleapplication,thesimplestwaytovalidatevariablesegmentvaluesistocheckthecontentsofthearrayintheviewmodel,asshowninListing4-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-6shows,embracedtheunexpectedvalueandaddedittotheviewmodel.

Page 87: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

87

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,thenIusethepushmethodtoaddthenewvalue.Ichangedtheviewmodelsothattheitemsarrayisanobservableitemusingtheko.observableArraymethod.Anobservablearrayislikearegularobservabledataitem,exceptthatbindingssuchasforeachareupdatedwhenthecontentofthearraychanges.UsinganobservablearraymeansthataddinganitemcausesKnockouttogeneratecontentandnavigationelementsinthedocument.

ThelaststepinthisprocessistocallthejQueryUIbuttonsetmethodagain.KOhasnoknowledgeofthejQueryUIstylesthatareappliedtoanaelementtocreateabutton,andthismethodhastobereappliedtogettherighteffect.Youcanseetheresultofnavigatingto#select/CherryinFigure4-4.

Figure4-4.Incorporatingunexpectedsegmentvaluesintotheapplicationstate

Page 88: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

88

UsingOptionalSegmentsThelimitationofvariablesegmentsisthattheURLmustcontainasegmentvaluetomatcharoute.Forexample,therouteselect/{item}willmatchanytwo-segmentURLwherethefirstsegmentisselect,butitwon’tmatchselect/Apple/Red(becausetherearetoomanysegments)orselect(becausetherearetoofewsegments).

Wecanuseoptionalsegmentstoincreasetheflexibilityofourroutes.Listing4-7showstheapplicationonanoptionalsegmenttotheexample.

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,theroutewillmatchURLsthathaveoneortwosegmentsandwherethefirstsegmentisselect.Ifthereisnosecondsegment,thentheargumentpassedtothefunctionwillbenull.Inmylisting,IdefaulttotheApplevalueifthisisthecase.Aroutecancontainasmanystatic,variable,andoptionalsegmentsasyourequire.Iwillkeepmyroutessimpleinthisexample,butyoucancreateprettymuchanycombinationyourequire.

AddingaDefaultRouteWiththeintroductionoftheoptionalsegment,myroutewillmatchone-andtwo-segmentURLs.ThefinalrouteIwanttoaddisadefaultroute,whichisonethatwillbeinvokedwhentherearenosegmentsintheURLatall.ThisisrequiredtocompletethesupportfortheBackbutton.ToseetheproblemIamsolving,loadthelistingintothebrowser,clickoneofthenavigationelements,andthenhittheBackbutton.Youcanseetheeffect—or,rather,thelackofaneffect—inFigure4-5.

Figure4-5.Navigatingbacktotheapplicationstartingpoint

Page 89: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

89

Theapplicationdoesn’tresettoitsoriginalstatewhentheBackbuttonisclicked.ThishappensonlywhenclickingtheBackbuttontakesthebrowserbacktothebaseURLforthewebapp(whichishttp://cheeselux.cominmycase).NothinghappensbecausethebaseURLdoesn’tmatchtheroutesthattheapplicationdefines.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.ClickingtheBackbuttonuntilthebaseURLisreachednowcausestheapplicationtoreturntoitsinitialstate.(Well,itreturnssortofbacktoitsoriginalstate;laterinthischapterI’llexplainawrinkleinthisapproachandshowyouhowtoimproveuponit.)

AdaptingEvent-DrivenControlstoNavigationItisnotalwayspossibletolimittheelementsinadocumentsothatallnavigationcanbehandledthroughaelements.WhenaddingJavaScripteventstoaroutedapplication,IfollowasimplepatternthatbridgesbetweenURLroutingandconventionaleventsandthatgivesmealotofthebenefitsof

Page 90: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

90

routingandletsmeuseotherkindsofelementsaswell.Listing4-9showsthispatternappliedtosomeotherelementtypes.

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-urlattributetotheelementswhoseeventsshouldresultinanavigationchange.IusejQuerytohandlethechangeandclickeventsforelementsthathavethedata-

Page 91: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

91

urlattribute.Handlingbotheventsallowsmetocaterforthedifferentkindsofinputelements.Iusethelivemethod,whichisaneatjQueryfeaturethatreliesoneventpropagationtoensurethateventsarehandledforelementsthatareaddedtothedocumentafterthescripthasexecuted;thisisessentialwhenthesetofelementsinthedocumentcanbealteredinresponsetoviewmodelchanges.Thisapproachallowsmetouseelementslikethis:

... <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.Icreatethevalueforthedata-urlattributewithmycustomformatAttrdatabinding,whichIdescribedearlier.Theselectelementrequiressomespecialhandlingbecausewhiletheselectelementtriggersthechangeevent,theinformationaboutwhichvaluehasbeenselectedisderivedfromthechildoptionelements.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,andtherestistakenfromthevalueattributeoftheoptionelements.Someelements,includingselect,triggerboththeclickandchangeevents,soIchecktoseethatthetargetURLdiffersfromthecurrentURLbeforeusinglocation.replacetotriggeranavigationchange.Listing4-10showshowthistechniquecanbeappliedtoselectelements,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>

Page 92: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

92

<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">

Page 93: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

93

<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>

Ihavedefinedanothercustombindingtocorrectlysettheselectedattributeontheappropriateoptionelement.Icalledthisbindingselected(obviouslyenough),anditisdefined,asshowninListing4-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) {

Page 94: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

94

if (accessor()) { $(element).siblings("[selected]").removeAttr("selected"); $(element).attr("selected", "selected"); } } }

Youmightbetemptedtosimplyhandleeventsandtriggertheapplicationchangesdirectly.Thisworks,butyouwillhavejustaddedtothecomplexityofyourapplicationbytakingontheoverheadorcreatingandmanagingroutesandkeepingtrackofwhicheventsfromwhichelementstriggerdifferencestatechanges.MyrecommendationistofocusonURLroutingandusebridging,asdescribedhere,tofunneleventsfromelementsintotheroutingsystem.

UsingtheHTML5HistoryAPITheCrossroadslibraryIhavebeenusingsofarinthischapterdependsontheHasherlibraryfromthesameauthortoreceivenotificationswhentheURLchanges.TheHasherlibrarymonitorstheURLandtellsCrossroadswhenitchanges,triggeringtheroutingbehavior.

Thereisaweaknessinthisapproach,whichisthatthestateoftheapplicationisn’tpreservedaspartofthebrowserhistory.Herearesomestepstodemonstratetheissue:

1. Loadthelistingintothebrowser.

2. ClicktheOrangebutton.

3. Navigatedirectlyto#select/Cherry.

4. ClicktheBananabutton.

5. ClicktheBackbuttontwice.

Everythingstartsoffwellenough.Whenyounavigatedtothe#select/CherryURL,thenewitemwasaddedtotheviewmodelandselectedproperly.WhenyouclickedtheBackbuttonthefirsttime,theCherryitemwascorrectlyselectedagain.TheproblemariseswhenyouclickedtheBackbuttonforthesecondtime.TheselecteditemwascorrectlywoundbacktoOrange,buttheCherryitemremainedonthelist.TheapplicationisabletousetheURLtoselectthecorrectitem,butwhentheOrangeitemwasselectedoriginally,therewasnoCherryitemintheviewmodel,andyetitisstilldisplayedtotheuser.

Forsomewebapplications,thiswon’tbeabigdeal,anditisn’tforthissimpleexample,either.Afterall,itdoesn’treallymatteriftheusercanselectanitemthattheyexplicitlyaddedinthefirstplace.Butforotherwebapps,thisisacriticalissue,andmakingsurethattheviewmodeliscorrectlypreservedinthebrowserhistoryisessential.WecanaddressthisusingtheHTML5HistoryAPI,whichgivesusmoreaccesstothebrowserhistorythanwebprogrammershavepreviouslyenjoyed.WeaccesstheHistoryAPIthroughthewindows.historyorglobalhistoryobject.TherearetwoaspectsoftheHistoryAPIthatIaminterestedinforthissituation.

Page 95: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

95

NoteIamnotgoingtocovertheHTML5APIbeyondwhatisneededtomaintainapplicationstate.IprovidefulldetailsinTheDefinitiveGuidetoHTML5,alsopublishedbyApress.YoucanreadtheW3Cspecificationathttp://dev.w3.org/html5/spec (theinformationontheHistoryAPIisinsection5.4,butthismaychangesincetheHTML5specificationisstillindraft).

Thehistory.replaceStatemethodletsyouassociateastateobjectwiththeentryinthebrowser’shistoryforthecurrentdocument.Therearethreeargumentstothismethod;thefirstisthestateobject,thesecondargumentisthetitletouseinthehistory,andthethirdistheURLforthedocument.Thesecondargumentisn’tusedbythecurrentgenerationofbrowsers,buttheURLargumentallowsyoutoeffectivelyreplacetheURLinthehistorythatisassociatedwiththecurrentdocument.ThepartIaminterestedinforthischapteristhefirstargument,whichIwillusetostorethecontentsoftheviewModel.itemsarrayinthehistorysothatIcanproperlymaintainthestatewhentheuserclickstheBackandForwardbuttons.

TipYoucanalsoinsertnewitemsintothehistoryusingthehistory.pushStatemethod.ThismethodtakesthesameargumentsasreplaceStateandcanbeusefulforinsertingadditionalstateinformation.

Thewindowbrowserobjecttriggersapopstateeventwhenevertheactivehistoryentrychanges.Iftheentryhasstateinformationassociatedwithit(becausethereplaceStateorpushStatemethodwasused),thenyoucanretrievethestateobjectthroughthehistory.stateproperty.

AddingHistoryStatetotheExampleApplicationThingsaren’tquiteassimpleasyoumightlikewhenitcomestousingtheHistoryAPI;itsuffersfromtwoproblemsthatarecommontomostoftheHTML5APIs.ThefirstproblemisthatnotallbrowserssupporttheHistoryAPI.Obviously,pre-HTML5browsersdon’tknowabouttheHistoryAPI,butevensomebrowserversionsthatsupportotherHTML5featuresdonotimplementtheHistoryAPI.

ThesecondproblemisthatthosebrowsersthatdoimplementtheHTML5APIintroduceinconsistencies,whichrequiressomecarefultesting.So,evenastheHistoryAPIhelpsussolveoneproblem,wearefacedwithothers.Evenso,theHistoryAPIisworthusing,aslongasyouacceptthatitisn’tuniversallysupportedandthatafallbackisrequired.Listing4-12showstheadditionoftheHistoryAPItothesimpleexamplewebapp.

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"/>

Page 96: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

96

<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); }); }

Page 97: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

97

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>

StoringtheApplicationStateThefirstsetofchangesinthelistingstorestheapplicationstatewhenthemainapplicationroutematchesaURL.ByrespondingtotheURLchange,IamabletopreservethestatewhenevertheuserclicksoneofthenavigationelementsorentersaURLdirectly.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); } }); ...

Page 98: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

98

ThenewscriptelementinthelistingaddstheModernizrlibrarytothewebapp.Modernizrisafeature-detectionlibrarythatcontainscheckstodeterminewhethernumerousHTML5andCSS3featuresaresupportedbythebrowser.YoucandownloadModernizrandgetfulldetailsofthefeaturesitcandetectathttp://modernizr.com.

Idon’twanttocallthemethodsoftheHistoryAPIunlessIamsurethatthebrowserimplementsit,soIcheckthevalueoftheModernizr.historyproperty.AvalueoftruemeansthattheHistoryAPIhasbeendetected,andavalueoffalsemeanstheAPIisn’tpresent.

Youcouldwriteyourownfeature-detectiontestsifyouprefer.Asanexample,hereisthecodebehindtheModernizr.historytest:

tests['history'] = function() { return !!(window.history && history.pushState); };

Modernizrsimplycheckstoseewhetherhistory.pushStateisdefinedbythebrowser.IprefertousealibrarylikeModernizrbecausethetestsitperformsarewell-validatedandupdatedasneededand,further,becausenotallofthetestsarequitesosimple.

TipFeature-detectionlibrariessuchasModernizrdon’tmakeanyassessmentofhowwellafeaturehasbeenimplemented.Thepresenceofthehistory.pushStatemethodindicatesthattheHistoryAPIispresent,butitdoesn’tprovideanyinsightsintoquirksorodditiesthatmayhavetobereckonedwith.Inshort,afeature-detectionlibraryisnosubstituteforthoroughlytestingyourcodeonarangeofbrowsers.

IftheHistoryAPIispresent,thenIcallthereplaceStatemethodtoassociatethevalueoftheviewmodelitemsarraywiththecurrentURL.IcanperformnoactioniftheHistoryAPIisn’tavailablebecausethereisn’tanalternativemechanismforstoringstateinthebrowser(althoughIcouldhaveusedapolyfill;seethesidebarfordetails).

USING A HISTORY POLYFILL

ApolyfillisaJavaScriptlibrarythatprovidessupportforanAPIforolderbrowsers.Pollyfilla,fromwhichthenameoriginates,istheU.K.equivalentoftheSpacklehome-repairproduct,andtheideaisthatapolyfilllibrarysmoothesoutthedevelopmentlandscape.Polyfilllibrariescanalsoworkarounddifferencesbetweenbrowserimplementationfeatures.TheHistoryAPImayseemlikeanidealcandidateforapolyfill,buttheproblemisthatthebrowserdoesn’tprovideanyalternativemeansofstoringstateobjects.ThemostcommonworkaroundistoexpressthestateaspartoftheURLsothatwemightendupwithsomethinglikethis:

http://cheeselux.com/#select/Banana?items=Apple,Orange,Banana,Cherry

Idon’tlikethisapproachbecauseIdon’tliketoseecomplexdatatypesexpressedinthisway,andIthinkitproducesconfusingURLs.Butyoumightfeeldifferently,orastatefulhistoryfeaturemaybecriticaltoyourproject.Ifthat’sthecase,thenthebestHistoryAPIpolyfillthatIhavefoundiscalledHistory.jsandisathttp://github.com/balupton/history.js.

Page 99: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

99

RestoringtheApplicationStateOfcourse,storingtheapplicationstateisn’tenough.Ialsohavetobeabletorestoreit,andthatmeansrespondingtothepopstateeventwhenitistriggeredbyaURLchange.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.historytocheckfortheAPIbeforeIusethebindmethodtoregisterahandlerfunctionforthepopstateevent.Thisisn’tstrictlynecessarysincetheeventsimplywon’tbetriggerediftheAPIisn’tpresent,butIliketomakeitobviousthatthisblockofcodeisrelatedtotheHistoryAPI.

Youcanseeanexampleofcateringtoabrowseroddityinthefunctionthathandlesthepopstateevent.Thehistory.statepropertyshouldreturnthestateobjectassociatedwiththecurrentURL,butGoogleChromedoesn’tsupportthis,andthevaluemustbeobtainedfromthestatepropertyoftheEventobjectinstead.jQuerynormalizesEventobjects,whichmeansthatIhavetousetheoriginalEventpropertytogettotheunderlyingeventobjectthatthebrowsergenerated,likethis:

var state = history.state ? history.state: event.originalEvent.state;

WiththisapproachIcangetthestatedatafromhistory.stateifitisavailableandtheeventifitisnot.Sadly,usingtheHTML5APIsoftenrequiresthiskindofworkaround,althoughIexpecttheconsistencyofthevariousimplementationswillimproveovertime.

Ican’trelyontherebeingastateobjecteverytimethepopstateeventistriggeredbecausenotallentriesinthebrowserhistorywillhavestateassociatedwiththem.

Whenthereisstatedata,IusetheremoveAllmethodtocleartheitemsarrayintheviewmodelandthenpopulateitwiththeitemsobtainedfromthestatedatausingthejQueryeachfunction:

Page 100: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

100

if (state) { viewModel.items.removeAll(); $.each(state, function(index, item) { viewModel.items.push(item); }); }

Oncethecontentoftheviewmodelhasbeenset,InotifyCrossroadsthattherehasbeenachangeinURLbycallingtheparsemethod.ThiswasthefunctionpreviouslyhandledbytheHasherlibrary,whichremovedtheleading#characterfromURLsbeforepassingthemtoCrossroads.IdothesametomaintaincompatibilitywiththeroutesIdefinedearlier:

crossroads.parse(location.hash.slice(1));

IwanttopreservecompatibilitybecauseIdon’twanttoassumethattheuserhasanHTML5browserthatsupportstheHistoryAPI.Tothatend,iftheModernizr.historypropertyisfalse,IfallbacktousingHashersothatthebasicfunctionalityofthewebappstillworks,evenifIcan’tprovidethestatemanagementfeature:

if (Modernizr.history) { ...History API code... } else { hasher.initialized.add(crossroads.parse, crossroads); hasher.changed.add(crossroads.parse, crossroads); hasher.init(); }

Withthesechanges,IamabletousetheHistoryAPIwhenitisavailabletomanagethestateoftheapplicationandunwinditwhentheuserusestheBackbutton.Figure4-6showsthekeystepfromthesequenceoftasksIhadyouperformatthestartofthissection.Astheusermovesbackthroughthehistory,theCherryitemdisappears.

Figure4-6.UsingtheHistoryAPItomanagechangesinapplicationstate

Asanaside,IchosetostoretheapplicationstateeverytimetheURLchangedbecauseitallowsmetosupporttheForwardbuttonaswellastheBackbutton.Fromthestateshowninthefigure,clickingtheForwardbuttonrestorestheCherryitemtotheviewmodel,demonstratingthattheapplicationstateisproperlypreservedandrestoredinbothdirections.

Page 101: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

101

AddingURLRoutingtotheCheeseLuxWebAppIswitchedtoasimpleexampleinthischapterbecauseIdidn’twanttooverwhelmtheroutingcode(whichisprettysparse)withthemarkupanddatabindings(whichcanbeverbose).ButnowthatIhaveexplainedhowURLroutingworks,itistimetointroduceittotheCheeseLuxdemo,asshowninListing4-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;

Page 102: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

102

}, 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, "",

Page 103: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

103

"#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>

Page 104: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

104

<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>

Iamnotgoingtobreakthislistingdownlinebylinebecausemuchoffunctionalityissimilartopreviousexamples.Thereare,however,acoupleoftechniquesthatareworthlearningandsomechangesthatIneedtoexplain,allofwhichI’llcoverinthesectionsthatfollow.Figure4-7showshowthewebappappearsinthebrowser.

Page 105: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

105

Figure4-7.AddingroutingtotheCheeseLuxexample

MovingthemapProductsFunctionThefirstchange,andthemostbasic,isthatIhavemovedthemapProductsfunctionintotheutil.jsfile.InChapter9,Iamgoingtoshowyouhowtopackageupthiskindoffunctionmoreusefully,andIdon’twanttokeeprecyclingthesamecodeinthelistings.AsImovedthefunction,Irewroteitsothatitcanworkonanysetofnestedarrays.Listing4-14showsthenewversionofthisfunction.

Listing4-14.TheRevisedmapProductsFunction

function mapProducts(func, data, indexer) { $.each(data, function(outerIndex, outerItem) { $.each(outerItem[indexer], function(itemIndex, innerItem) { func(innerItem, outerItem); }); }); }

Thetwonewargumentstothefunctionaretheouternestedarrayandthepropertynameoftheinnerarray.YoucanseehowIhaveusedthisinthemainlistingsothattheargumentsarecheeseModel.productsanditems,respectively.

EnhancingtheViewModelImadetwochangestotheviewmodel.Thefirstwastodefineanobservabledataitemtocapturetheselectedcheesecategory:

cheeseModel.selectedCategory = ko.observable(cheeseModel.products[0].category);

Page 106: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

106

Thesecondismuchmoreinteresting.Databindingsarenotthemeansbywhichviewmodelchangesarepropagatedintothewebapp.Youcanalsosubscribetoanobservabledataitemandspecifyafunctionthatwillbeexecutedwhenthevaluechanges.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,theupdateStatefunctionwillbeexecuted.I’lldescribethisfunctionshortly.Subscriptionsareratherlikeeventsfortheviewmodel;theycanbeusefulinanynumberofsituations,andIoftenfindmyselfusingthemwhenIwantsometaskperformedautomatically.

ManagingApplicationStateIwanttopreservetwokindsofstateinthiswebapp.Thefirstistheselectedproductcategory,andthesecondisthecontentsofthebasket.Istorestateinformationinthebrowser’shistoryintheupdateStatefunction,whichisexecutedwhenevermyquantitysubscriptionistriggeredortheselectedcategorychanges.

TipThetechniquethatIdemonstratehereisalittleoddwhenappliedtoashoppingbasket,becausewebsiteswillusuallygotogreatlengthstopreserveyourproductselections.Ignorethis,ifyouwill,andfocusonthestatemanagementtechnique,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()); }

Page 107: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

107

TipThislistingrequirestheHTML5HistoryAPI,andunliketheearlierexamplesinthischapter,thereisnofallbacktotheHTML4-compatibleapproachtakenbytheHasherlibrary.

Icreateanobjectthathasacategorypropertythatcontainsthenameoftheselectedcategoryandonepropertyforeachindividualcheesethathasanonzeroquantityvalue.IwritethistothebrowserhistoryusingthereplaceStatemethod,whichIhavehighlightedinthelisting.

Somethingcleverishappeninghere.ToexplainwhatIamdoing—andwhy—wehavetostartwiththemarkupforthenavigationelementsthatremoveproductsfromthebasket.HereistherelevantHTML:

<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.Iusethesecondsegmenttofindtherightitemintheviewmodelandchangethevalueofthequantitypropertytozero.

Atthispoint,Ihaveaproblem.IhavenavigatedtoaURLthatIdon’twanttheusertobeabletonavigatebacktobecauseitwillmatchtheroutethatjustremovesitemsfromthebasket,andthatdoesn’thelpme.

Thesolutionisinthecalltothehistory.replaceStatemethod.Whenthequantityvalueischanged,mysubscriptioncausestheupdateStatefunctiontobecalled,whichinturncallshistory.replaceState.Thethirdargumentistheimportantone:

history.replaceState(state, "", "#select/" + cheeseModel.selectedCategory());

TheURLspecifiedbythisargumentisusedtoreplacetheURLthattheusernavigatedto.Thebrowserdoesn’tnavigatetotheURLwhenitischanged,butwhentheusermovesbackthroughthebrowserhistory,itisthereplacementURLthatwillbeusedbythebrowser.IrrespectiveofwhichroutematchestheURL,thehistorywillalwayscontainonethatstartswith#select/.Inthisway,IcanuseURLroutingwithoutexposingtheinnerworkingsofmywebapptotheuser.

Page 108: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER4USINGURLROUTING

108

SummaryInthischapter,IhaveshownyouhowtoaddURLroutingtoyourwebapplications.ThisisapowerfulandflexibletechniquethatseparatesapplicationnavigationfromHTMLelements,allowingforamoreconciseandexpressivewayofhandlingnavigationandamoretestableandmaintainablecodebase.Itcantakeawhiletogetusedtousingroutingattheclient,butitiswellworththeinvestmentoftimeandenergy,especiallyforlargeandcomplexprojects.

Page 109: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

C H A P T E R 5

109

Creating Offline Web Apps

TheHTML5specificationincludessupportfortheApplicationCache,whichisusedtocreatewebapplicationsthatareavailabletousersevenwhennonetworkconnectionisavailable.Thisisidealifyourusersneedtoworkofflineorinenvironmentswhereconnectivityisconstrained(suchasonanairplane,forexample).

AswithallofthemorecomplexHTML5features,usingtheapplicationcacheisn’tentirelysmoothsailing.Therearesomedifferencesinimplementationsbetweenbrowsersandsomeodditiesthatyouneedtobeawareof.Inthischapter,I’llshowyouhowtocreateaneffectiveofflinewebapplicationandhowtoavoidvariouspitfalls.

CautionThebrowsersupportforofflinestorageisatanearlystage,andtherearealotofinconsistencies.Ihavetriedtopointoutpotentialproblems,butbecauseeachbrowserreleasetendstorefinetheimplementationofHTML5features,youshouldexpecttoseesomevariationswhenyouruntheexamplesinthischapter.

ResettingtheExampleOnceagain,IamgoingtosimplifytheCheeseLuxexamplesothatIamnotlistingreamsofcodethatrelatetootherchapters.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"/>

Page 110: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER5CREATINGOFFLINEWEBAPPS

110

<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>

Page 111: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER5CREATINGOFFLINEWEBAPPS

111

</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,butIhavesimplifiedsomeofthefunctionality.Insteadofabasket,Ihaveaddedatotaldisplaytothebottomofeachcategoryofcheese.IhavemovedthecodethatcreatestheobservableviewmodelitemsintoafunctioncalledenhanceViewModelintheutils.jsfile.Everythingelseinthislistingshouldbeself-evident.

UsingtheHTML5ApplicationCacheThestartingpointforusingtheapplicationcacheistocreateamanifest.Thistellsthebrowserwhichfilesarerequiredtoruntheapplicationofflinesothatthebrowsercanensurethattheyareallpresentinthecache.Manifestfileshavetheappcachefilesuffix,soIhavecalledmymanifestfilecheeselux.appcache.YoucanseethecontentsofthisfileinListing5-2.

Page 112: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER5CREATINGOFFLINEWEBAPPS

112

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 MANIFESTheaderandthenlistsallthefilesthattheapplicationrequires,includingtheHTMLfilewhosehtmlelementcontainsthemanifestattribute(discussedinamoment).Inthelisting,Ihavebrokenthefilesdownbytypeandusedcomments(whicharelinesstartingwiththe#character)tomakeiteasiertofigureoutwhat’shappening.

TipYouwillnoticethatIhavecommentedouttheentryfortheblackwave.pngfile.Iusethisfiletodemonstratethebehaviorofacachedapplicationinamoment.

ThemanifestisaddedtotheHTMLdocumentthroughthemanifestattributeofthehtmlelement,asListing5-3shows.

Page 113: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER5CREATINGOFFLINEWEBAPPS

113

Listing5-3.AddingtheManifesttotheHTMLDocument

<!DOCTYPE html> <html manifest="cheeselux.appcache"> <head> ... </head> <body> ... </body> </html>

WhentheHTMLdocumentisloaded,thebrowserdetectsthemanifestattribute,requeststhespecifiedappcachefilefromthewebserver,andbeginsloadingandcachingeachfilelistedinthemanifestfile.Thefilesthataredownloadedwhenthebrowserprocessesthemanifestarecalledtheofflinecontent.Somebrowserswillprompttheuserforpermissiontostoreofflinecontent.

CautionBecarefulwhenyoucreatethemanifest.Ifanyoftheitemslistedcannotbeobtainedfromtheserver,thenthebrowserwillnotcachetheapplicationatall.

UnderstandingWhenCachedContentIsUsedTheofflinecontentisn’tusedwhenitisfirstloadedbythebrowser.Itiscachedforthenexttimethattheuserloadsorreloadsthepage.Thenameofflinecontentismisleading.Oncethebrowserhasofflinecontentforawebapp,itwillbeusedwhenevertheuservisitsthewebapp’sURL,evenwhenthereisanetworkconnectionavailable.Thebrowsertakesresponsibilityforensuringthatthelatestversionoftheofflinecontentisbeingused,butasyou’lllearn,thisisacomplicatedprocessandrequiressomeprogrammerintervention.

Icommentedouttheblackwave.pngfileinthemanifesttodemonstratehowthebrowserhandlesofflinecontent.Iuseblackwave.pngasthebackgroundimagefortheCheeseLuxwebapp,andthisgivesmeanicewaytodemonstratethebasicbehaviorofacachedwebapplication.

Tostartwith,addthemanifestattributetotheexampleasshowninListing5-3,andloadthedocumentintoyourbrowser.Differentbrowsersdealwithcachedapplicationsindifferentways.Forexample,GoogleChromewillquietlyprocessthemanifestandstartdownloadingthecontentitspecified.MozillaFirefoxwillusuallyprompttheusertoallowofflinecontent,asshowninFigure5-1.IfyouareusingFirefox,clicktheAllowbuttontostartthebrowserprocessingthemanifest.

Page 114: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER5CREATINGOFFLINEWEBAPPS

114

Figure5-1.Firefoxpromptingtheusertoallowthewebapptostoredatalocally

TipAllofthemainstreambrowsersallowtheusertodisablecachedapplications,whichmeansyoucannotrelyonbeingabletostoredataevenifthebrowserimplementsthefeature.Insuchcases,theapplicationmanifestwillsimplybeignored.Youmayneedtochangetheconfigurationofyourbrowsertocachetheexamplecontent.

YoushouldseetheCheeseLuxwebappwiththeblackbackground.Atthispoint,thebrowserhastwocopiesofthewebapp.Thefirstcopyisintheregularbrowsercache,andthisistheversionthatiscurrentlyrunning.Thesecondcopyisintheapplicationcacheandcontainstheitemsspecifiedinthemanifest.Simplyreloadthepagetoswitchtotheapplicationcacheversion.Whenyoudoreload,thebackgroundwillbewhite,asshowninFigure5-2.

Figure5-2.Switchingtotheapplicationcache

Thedifferenceiscausedbythefactthattheblackwave.pngfileiscommentedoutinthemanifest.Thebrowserkeepstheapplicationcacheandtheregularcacheseparate,whichmeansthateventhoughithasablackwave.pngfileintheregularcache,itwon’tuseitforacachedapplication.

TipNoticethatyouhavenotdoneanythingtothenetworkconnection.Thebrowserisstillonline,buttheapplicationhasbeenloadedusingsolelyofflinecontent.ThisissomethingthatI’llreturntosoon.

Page 115: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER5CREATINGOFFLINEWEBAPPS

115

AcceptingChangestotheManifestThemostsignificantchangeinbehaviorforacachedapplicationisthatrefreshingthewebpagedoesn’tcausetheapplicationcontenttobecached.Theideaisthatupdatestoacachedapplicationneedtobemanagedtoavoidinconsistentchanges.Uncommentingtheblackwave.pnglineinthemanifestandreloading,forexample,wouldn’tchangethebackgroundtoblack.

Listing5-4showstheminimumamountofcodethatisneededinawebapptosupportupdates.I’llshowyouhowtousemoreoftheApplicationCacheAPIlaterinthechapter,butweneedthesechangesbeforewecangoanyfurther.

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>

Page 116: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER5CREATINGOFFLINEWEBAPPS

116

...

TheHTML5ApplicationCacheAPIisexpressedthroughthewindow.applicationCachebrowserobject.Thisobjecttriggerseventstoinformthewebappofchangesinthecachestatus.Themostimportantforusatthemomentistheupdatereadyevent,whichmeansthatthereisupdatedcachedataavailable.Inadditiontotheevents,theapplicationCacheobjectdefinessomeusefulmethodsandproperties.Onceagain,I’llreturntotheselaterinthechapter,butthemethodIcareaboutnowisswapCache,whichappliestheupdatedmanifestanditscontentstotheapplicationcache.

Iamnowreadytodemonstrateupdatingacachedwebapplication.ButbeforeIdo,Imustremovetheexistingcacheddata.IhavecreatedazombiewebappbyapplyingamanifestwithoutaddingthecalltotheswapCachemethod,andthereisnowayIcangetupdatestotakeeffect.Ineedtoclearthecacheandstartagain.ThereisnowaytoclearthecacheusingJavaScript,andthebrowserhasadifferentmechanismformanuallyclearingapplicationcachedata.ForGoogleChrome,youdeletetheregularbrowsinghistory.ForMozillaFirefox,youmustselecttheAdvanced➤Networkoptionstab,selectthewebsitefromthelist,andclicktheRemovebutton.

Onceyouhaveclearedtheapplicationcache,reloadthelistingtoloadthemanifestandcachethedata.Reloadthepageagaintoswitchtothecachedversionoftheapplication(whichwillhavethewhitebackground).

Finally,youcanuncommenttheblackwave.pngentryinthecheeselux.appcachefile.Atthispoint,youwillneedtoreloadthewebpagetwice.Thefirsttimecausesthebrowsertocheckforanupdatedmanifest,findthatthereisanewversion,anddownloadtheupdatedresourcesintothecache.Atthispoint,theupdatereadyeventistriggered,andmyscriptcallstheswapCachemethod,applyingtheupdatestothecache.Thosechangesdon’ttakeeffectuntilthenexttimethatthewebappisloaded,whichiswhythesecondreloadisrequired.Thisisanawkwardapproach,butI’llshowyouhowtoimproveuponitshortly.Atthispoint,thecachewillhavebeenupdatedwithamanifestthatdoesincludetheblackwave.pngfile,andthewebappbackgroundwillhaveturnedblack.

TipThebrowsercheckstoseeonlyifthemanifestfilehaschanged.Changestoindividualresources,includingHTMLandscriptfiles,areignoredunlessthemanifestalsochanges.Ifthemanifesthaschanged,thenthebrowserwillchecktoseewhethertheindividualresourceshavebeenupdatedsincetheywerelastdownloaded(and,ofcourse,willdownloadanyresourcesthathavebeenaddedtothemanifest).

TakingControloftheCacheUpdateProcessItookyouthelongwayaroundtheupdatesbecauseIwantedtoemphasizethewayinwhichthebrowsertriestoisolateusfromhavingtodealwithaninconsistentcache.ThereisnostandardwayforaJavaScriptwebapptorespondtoacachechangewhileitisrunning,sotheHTML5ApplicationCachestandarderrsonthesideofcaution,andcacheupdatesareappliedonlywhentheapplicationisloaded.

Page 117: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER5CREATINGOFFLINEWEBAPPS

117

CautionThecurrentimplementationsoftheapplicationcachearefineforusebynormalusers,buttheytendtostruggleduringthedevelopmentphasewhentherearelotsofchangestothemanifestandlotsofupdatesappliedtothecache.Therewillcomeapointwhereyoustartgettingoddbehavior,andnochangesyoumaketoyourmanifestoryourapplicationwillsortmattersout.Whenthishappens,thesimplestthingtodoistoclearthebrowserhistoryandapplicationcachecontentsandseewhethertheproblemspersist.Mostofthetime,Ifindthatsuddenchangesinbehaviorarecausedbythebrowserandthatstartingoverfixesthings(althoughthissometimesrequiresclearingthefilesdirectlyfromthediskusingthefileexplorer,becausethebrowser’sabilitytomanagetheapplicationcachealsogoesawry).

WecanusetheapplicationCachebrowserobjecttomanageacachedapplicationinamoreelegantway.Thefirstthingwecandoistomonitorthestatusofthecacheandpresenttheuserwithsomeoptions.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},

Page 118: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER5CREATINGOFFLINEWEBAPPS

118

{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>

Page 119: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER5CREATINGOFFLINEWEBAPPS

119

</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,whichrepresentsthestateoftheapplicationcache:

cache: { status: ko.observable(window.applicationCache.status) }

IamusingtheviewmodelbecauseIwanttodisseminatethestatusintotheHTMLmarkupusingdatabindings.Tokeepthevalueup-to-date,Isubscribetoasetofeventstriggeredbythewindow.applicationCacheobject,likethis:

Page 120: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER5CREATINGOFFLINEWEBAPPS

120

$(window.applicationCache).bind("checking noupdate downloading " + "progress cached updateready", function(e) { cheeseModel.cache.status(window.applicationCache.status); });

Sevencacheeventsareavailable.IhavelistedtheminTable5-1.Ihaveusedthebindmethodtohandlesixofthem,becausetheseventh,obsolete,arisesonlywhenthemanifestfileisn’tavailablefromthewebserver.

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,andIhavedescribedtherangeofvaluesthatarereturnedinTable5-2.

Table5-2.ValuesReturnedbytheapplicationCache.statusProperty

Value Name Description

0 UNCACHED Returnedforwebappsthatdonotspecifyamanifestorwhenthereisamanifestbuttheofflinecontenthasnotbeendownloaded.

1 IDLE Thecacheisnotperforminganyaction.Thisisthedefaultvalueoncetheofflinecontenthasbeendownloadedandcached.

2 CHECKING Thebrowserischeckingforanupdatedmanifest.

3 DOWNLOADING Thebrowserisdownloadingupdatedofflinecontent.

4 UPDATEREADY Thereisupdatedofflinecontentwaitingtobeappliedtothecache.

5 OBSOLETE Thecacheddataisobsolete.

Page 121: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER5CREATINGOFFLINEWEBAPPS

121

Asyoucansee,thestatusvaluescorrespondwithsomeoftheapplicationcacheevents.Forthisexample,IcareonlyabouttheUPDATEREADYstatusvalue,whichIusetocontrolthevisibilityofsomeaelementsIaddedtothelogoareaofthepage:

<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,andwhenthereisanupdateavailable,Iprompttheusertoinstallit.Figure5-3showsbothofthesebuttonsinsitu.

Figure5-3.Addingbuttonstocontrolthecache

Asyoucanseeinthefigure,IhaveusedjQueryUItocreatebuttonsfromtheaelements.IhavealsousedthejQueryclickmethodtoregisterahandlerfortheclickevent,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); } });

IhaveusedregularJavaScripteventstocontrolthecachebecauseIwanttheusertobeabletocheckforupdatesrepeatedly.BrowsersignorerequeststonavigatetothesameinternalURLthatisbeingdisplayed.Youcanseethishappeningifyouclickoneofthecheesecategorybuttons.Clickingthesamebuttonrepeatedlydoesn’tdoanything,andthebuttoniseffectivelydisableduntilanothercategoryisselected.IfIhadusedURLroutingtodealwiththecachebuttons,thentheuserwouldbeabletocheckforanupdateonceandthennotbeabletodosoagainuntiltheynavigatedtoanotherinternalURL(whichforthisexamplewouldrequireselectingacheesecategory).So,instead,IusedJavaScripteventsthataretriggeredeverytimethebuttonisclicked,irrespectiveoftherestoftheapplicationstate.

Page 122: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER5CREATINGOFFLINEWEBAPPS

122

Wheneithercachebuttonisclicked,Ireadthevalueofthedata-actionattribute.Iftheattributevalueisupdate,thenIcallthecacheupdatemethod.Thiscausesthebrowsertocheckwiththeservertoseewhetherthemanifesthaschanged.Ifithas,thenthestatusofthecachewillchangetoUPDATEREADY,andtheApplyUpdatebuttonwillbeshowntotheuser.

WhentheApplyUpdatebuttonisclicked,IcalltheswapCachemethodtopushtheupdatesintotheapplicationcache.Theseupdateswon’ttakeeffectuntiltheapplicationisreloaded,whichIforcebycallingthewindow.location.reloadmethod.Thismeanstheupdatesareappliedtothecacheandimmediatelyusedinresponsetoasingleactionbytheuser.Thesimplestwaytotesttheseadditionsistotogglethestatusoftheblackwave.pngimageinthemanifestandapplytheresultingupdate.Seetheinformationonthecachecontrolheaderifyouwanttotestmoresubstantialchanges.

APPLICATION CACHE ENTRIES AND THE CACHE-CONTROL HEADER

CallingtheapplicationCachemethoddoesn’talwayscausethebrowsertocontacttheservertoseewhetherthemanifesthaschanged.AllofthemainstreambrowsershonortheHTTPCache-Controlheaderandwillcheckforupdatesonlywhenthelifeofthemanifesthasexpired.

Further,evenifthemanifesthaschanged,thebrowserhonorstheCache-Controlvalueforindividualmanifestitems.ThiscanleadtoasituationwhereanupdatetoanHTMLorscriptfileisignoredifthemanifestchangeswithintheCache-Controllifetimeoftheaffectedresource.

Inproduction,thisbehaviorisperfectlyreasonable.Butduringdevelopmentandtesting,it’sahugepainsincechangesmadetothecontentsofHTMLandscriptfileswon’tbeimmediatelyreflectedinanupdate.Togetaroundthis,IhavesetaveryshortcachelifeonthecontentservedbytheNode.jsserver.You’llneedtodosomethingsimilartoyourdevelopmentserverstogetthesameeffect.

AddingNetworkandFallbackEntriestotheManifestRegularmanifestentriestellthebrowsertoproactivelyobtainandcacheresourcesthatthewebapprequires.Inaddition,theapplicationcachesupportstwoothermanifestentrytypes:networkandfallbackentries.Networkentries,alsoknownaswhitelistentries,specifyaresourcethatthebrowsershouldnotcache.Requestsfortheseresourceswillalwaysresultinarequesttotheserverwhilethebrowserisonline.Thisisusefultoensurethattheuseralwaysreceivesthelatestversionofafile,eventhoughtherestoftheapplicationiscached.

Thefallbackentriestellthebrowserwhattodowhenthebrowserisofflineandtheuserrequestsanetworkentry.Fallbackentriesallowyoutosubstituteanalternativefileratherthandisplayinganerrortotheuser.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

Page 123: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER5CREATINGOFFLINEWEBAPPS

123

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.Ihavecreatedabuttonthatlinkstothisfileintheexample.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.YoucanseetheeffectinFigure5-4.

Page 124: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER5CREATINGOFFLINEWEBAPPS

124

Figure5-4.Linkingtothenews.htmlpage

BecauseitisintheNETWORKsection,thenews.htmlfileisneveraddedtotheapplicationcache.WhenIclicktheNewsbutton,thebrowseractsasitwouldforregularcontent.Itcontactstheserver,getstheresources,andaddsthemtotheregular(nonapplication)cache,beforeshowingthemtotheuser.Icanmakechangestothenews.htmlfile,andtheywillbedisplayedtotheuserevenwhentheapplicationcachehasn’tbeenupdated.

Whenthebrowsergoesoffline,thereisnowaytogetholdofthecontentthatisnotintheapplicationcache.ThisiswheretheFALLBACKentriescomein.Theformatoftheseentriesisdifferentfromtheothers.

CautionBrowserstakedifferentviewsaboutwhatbeingofflinemeans.Iexplainmoreaboutthisinthe“MonitoringOfflineStatus”sectionlaterinthischapter.

Thefirstpartspecifiesaprefixforresources,andthesecondpartspecifiesafiletousewhenaresourcethatmatchestheprefixisrequestedwhilethebrowserisoffline.So,inListing5-7,IhavesetthemanifestsothatanyrequesttoanyURL(representedby/)shouldbegiventhefileoffline.htmlinstead.

Page 125: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER5CREATINGOFFLINEWEBAPPS

125

Listing5-7.UsingaFallbackEntryintheApplicationManifest

... # fonts fonts/YanoneKaffeesatz-Regular.ttf fonts/fanwood_italic-webfont.ttf fonts/ostrich-rounded-webfont.woff FALLBACK: / offline.html

TipBrowsershandlefallbackforresourcesinthenetworkinconsistently.YoushouldnotrelyonthefallbacksectiontoprovidesubstitutecontentforURLsthatarelistedinthenetworksection,onlythosethatareinthemainpartofthemanifest.Supportforprovidingfallbacksforindividualfilesisalsoinconsistent,whichiswhyIhaveusedthebroadestpossiblefallbackintheexamplesforthischapter.IexpectthereliabilityandconsistencyofthesefeaturestoimproveastheHTML5implementationsstabilize.

Whenthebrowserisoffline,clickingtheNewsbuttontriggersarequestforaURLthatthebrowsercannotservicefromtheapplicationcache,andthefallbackentryisusedinstead.YoucanseetheresultinFigure5-5.TheURLinthebrowseraddressbarshowstheURLthatwasrequested,butthecontentthatisshownisfromthefallbackresource.

Figure5-5.Usingthefallbackentry

Page 126: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER5CREATINGOFFLINEWEBAPPS

126

TheHTML5ApplicationCachespecificationprovidessupportformorecomplexfallbackentries,includingper-URLfallbacksandtheuseofwildcards.However,asIwritethis,GoogleChromedoesn’tsupporttheseentries,andageneralfallback,suchasIhaveshowninthelisting,isallthatcanbereliablyused.

ThespecificationfortheHTML5ApplicationCachefeatureisambiguousaboutwhetherthebrowsershouldusetheregularcontentcachetosatisfyrequestsfornetworkentryresources.And,ofcourse,differentapproacheshavebeenadopted.GoogleChrometakesthemostliteralinterpretationofthestandard.Whenthebrowserisoffline,networkentryresourcesarenotavailabletothewebapp.MozillaFirefoxandOperatakeamoreforgivingapproach:iftheresourceisinthemainbrowsercachewhenthebrowsergoesoffline,itwillbeavailabletothewebapp.Ofcourse,thebrowsersareupdatedfrequently,sotheremightbeadifferentsetofbehaviorsbythetimeyoureadthis.

CautionTheimplementationofthenetworkandfallbackfeaturescanbeinconsistent.Therearesomeodditiesintheimplementationsofthemainstreambrowsers,andasaconsequence,Itendtoavoidusingthesekindsofentriesforcachedapplications.Theregularcacheentriesworkwell,however,andcanberelieduponinthosebrowsersthatsupporttheapplicationcachefeature.

MonitoringOfflineStatusHTML5definestheabilitytodeterminewhetherthebrowserisonline.Whatbeingofflinemeansdependsontheplatformandthebrowser.Formobiledevices,beingofflineusuallyrequirestheusertoswitchtoairplanemodeortoexplicitlyswitchoffnetworkinginsomeotherway.Simplybeingoutofcoveragedoesn’tusuallychangethebrowserstatus.

Explicituseractionisrequiredformostdesktopbrowsersaswell.Forexample,FirefoxandOperabothhavemenuitemsthattogglethebrowserbetweenonlineandofflinemodes.TheexceptionisGoogleChrome,whichmonitorstheunderlyingnetworkconnectionsandswitchestoofflineifnonetworkdevicesareenabled.

NoteChromewillgointoofflinemodeonlywhenthereisnoenablednetworkconnection.Tocreatethescreenshotinthissection,Ihadtodisablemymain(wireless)connection,manuallydisableanEthernetportthatwasenabledbutnotpluggedintoanything,anddisableaconnectioncreatedbyavirtualmachinepackage.OnlythendidChromedecideitwastimetogooffline.Mostuserswon’thavethisproblem,butitissomethingtobearinmind,especiallyifyouarenotgettingtheofflinebehavioryouexpect.

RecentversionsofthemainstreambrowsersimplementanHTML5featurethatreportsonwhetherthebrowserisonlineoroffline.Thisisusefulbothintermsofpresentingtheuserwithausefulandcontextualinterfaceandintermsofmanagingtheinternaloperationsofthewebapp.Todemonstratethisfeature,IamgoingtochangetheexamplewebappsothatthecachecontrolandNewsbuttonsaredisplayedonlywhenthebrowserisonline.Listing5-8showsthechangestothescriptelement.

Page 127: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER5CREATINGOFFLINEWEBAPPS

127

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") {

Page 128: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER5CREATINGOFFLINEWEBAPPS

128

window.applicationCache.update(); } else { window.applicationCache.swapCache(); window.location.reload(false); } }); }); </script>

Thewindowbrowserobjectsupportstheonlineandofflineeventsthataretriggeredwhenthebrowserstatuschanges.Youcangetthecurrentstatusthroughthewindow.navigator.onLineproperty,whichreturnstrueifthebrowserisonlineandfalseifitisoffline.NotethattheLinonLineisuppercase.Ihaveaddedanonlineobservabledataitemtotheviewmodel,whichIupdateinresponsetotheonlineandofflineevents.ThisisthesametechniquethatIusedfortheapplicationcachestatus,anditallowsmetousetheviewmodeltopropagatechangesthroughtomymarkup.Listing5-9showsthechangestotheHTMLelementsthatdisplaytheNewsandapplicationcachecontrolbuttons.

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.Whenthebrowserisoffline,Ireplacethebuttonswithasimpleplaceholder.YoucanseetheeffectinFigure5-6.

TipYouneedtoensurethatyouhavetherightversionoftheofflinecontentbeforetakingthebrowseroffline.Beforerunningthisexample,youshouldeitherchangethemanifestorclearthebrowser’shistory.

Page 129: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER5CREATINGOFFLINEWEBAPPS

129

Figure5-6.Respondingtothebrowseronlinestatus

USING RECURRING AJAX REQUESTS POLYFILLS

ThereareJavaScriptpolyfilllibrariesavailablethatuseperiodicAjaxrequestsasasubstituteforthenavigator.onLineproperty.Arequestforasmallfileismadetotheservereveryfewminutes,andiftherequestfails,thebrowserisassumedtobeoffline.

Istronglyrecommendavoidingthisapproach.First,itisn’tresponsiveenoughtobeuseful.Ifyouaretryingtoworkoutwhenthebrowserisoffline,findingoutseveralminutesafterithappensisn’tmuchuse.Duringtheperiodsbetweentests,thestatusofthebrowserisunknownandcannotbereliedon.

Second,repeatedlyrequestingafileconsumesbandwidththatyouandtheuserhavetopayfor.Ifyouhaveapopularwebapp,thebandwidthcostsofperiodiccheckscanbesignificant.Moreimportantly,asunlimiteddataplansformobiledevicesbecomelesscommon,assumingthatyoucanmakefreeuseofyourusers’bandwidthisextremelypresumptuous.Myadviceistonotrelyonthissortofpolyfill.Justdowithoutthenotificationsifthebrowserdoesn’tsupportthem.

UnderstandingwithAjaxandPOSTRequestsTheapplicationcachemakesitdifficulttoworkwithAjaxand,morebroadly,postingformsingeneral.Andthingsgetworsewhenthebrowserisoffline,althoughperhapsnotinthewayyoumightexpect.Inthissection,I’llshowyoutheproblemsandthelimitedoptionsthatareavailabletodealwiththem.First,however,IneedtoupdatetheCheeseLuxwebappsothatitdependsonanAjaxGETrequesttooperate.Listing5-10showstherequiredchangedtothescriptelement(nochangesareneededtothemarkupforthisexample).

Page 130: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER5CREATINGOFFLINEWEBAPPS

130

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); }); $('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); } }); }); }); </script> ...

Page 131: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER5CREATINGOFFLINEWEBAPPS

131

Inthislisting,IhaveusedthejQuerygetJSONmethod.ThisisaconveniencemethodthatmakesanAjaxGETrequestfortheJSONfilespecifiedbythefirstmethodargument,whichisproducts.jsoninthiscase.WhentheAjaxrequestshascompleted,jQueryparsestheJSONdatatocreateaJavaScriptobject,whichispassedtothefunctionspecifiedbythesecondmethodargument.Inmylisting,thefunctionsimplytakestheJavaScriptobjectandassignsittotheproductspropertyoftheviewmodel.Theproducts.jsonfilecontainsasupersetofthedataIhavebeendefininginline.Thesamecategories,products,andpricesaredefined,alongwithanadditionaldescriptionofeachcheese.Listing5-11showsanextractfromproducts.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.ThesuccessmethodispartofthejQuerysupportforJavaScriptPromises,whichmakeiteasytouseandmanageasynchronousoperationslikeAjaxrequests.Thefunctionpassedtothesuccessmethodwon’tbeexecuteduntilthegetJSONmethodhascompleted,ensuringthatmyviewmodeliscompletebeforetherestofmyscriptisrun.

ThisapproachtogettingcoredatafromJSONisacommonone,especiallywherethedataissourcedfromadifferentsetofsystemstotherestofthewebapp.And,ifusedcarefully,itcanensurethattheuserhasthemostrecentdatabutstillhasthebenefitofacachedapplication.

UnderstandingtheDefaultAjaxGETBehaviorThebrowsertreatsanAjaxGETrequestinaverysimpleway.TherequestwillfailiftheAjaxrequestisforaresourcethatisnotinthemanifest,evenwhenthebrowserisonline.

Formyexampleapplication,thismeansthatdataisreturnedfromtherequestanditdiesahorribledeath.ThefunctionIpassedasanargumenttothegetJSONmethodisexecutedonlyiftheAjaxrequestsucceeds,andthesameistrueforthefunctionpassedtothesuccessmethod.Becauseneitherfunctionisexecuted,themainpartofmyscriptcodeisn’tperformed,andIleavetheuserstranded.Worse,sincetheapplicationcachecontrolbuttonsareneversetup,Idon’tgivetheuserameanstoupdatetheapplicationtofixtheproblem.

Ihaveshownthisscenariobecauseitisverycommonlyencounteredwhenprogrammersfirststartusingtheapplicationcache.I’llshowyouhowtomaketheAjaxconnectionworkshortly,butfirst,thereareacoupleofimportantchangestobemade.

RestructuringtheApplicationThefirstchangeistostructuretheapplicationsothatthecorebehaviorthatwillgettheuserbackoutoftroublewillalwaysbeexecuted.Myinitiallistingisjusttoooptimistic,andIneedtoseparatethosepartsofthecodethatshouldalwaysberun.Therearelotsofdifferenttechniquesfordoingthis,butIfindthesimplestisjusttocreateanotherfunctionthatiscontingentonthejQueryreadyevent.Listing5-12showsthechangesIrequiretothescriptelement.

Page 132: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER5CREATINGOFFLINEWEBAPPS

132

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); } }); }); });

Page 133: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER5CREATINGOFFLINEWEBAPPS

133

</script> ...

Ihavepulledallofthecodethatisn’tcontingentonasuccessfulAjaxrequesttogetherandplaceditinafunctionpassedtothecompletemethod,whichIaddtothechainofmethodcalls.ThisfunctionwillbeexecutedwhentheAjaxrequestfinishes,irrespectiveofwhetheritsucceededorfailed.

Now,evenwhentheAjaxrequestfails,thecontrolsforupdatingthecacheandapplyingchangesarealwaysavailable.GiventhatAjaxproblemsarethemostlikelyreasonforerrorsattheclient,givingtheuserawaytoapplyanupdateisessential.Otherwise,youaregoingtohavetoprovideper-browserinstructionsforclearingthecache.Itisnotaperfectsolution,becauseIamunabletoapplymydatabindings,soelementsthatIwouldratherwerehiddenarevisible.IcouldusetheCSSdisplaypropertytohidesomeoftheseitems,butIthinkjustgivingtheusertheabilitytodownloadandapplyanupdateiswhatisessential.YoucanseetheeffectbeforeandaftertherestructuringinFigure5-7.

Figure5-7.Theeffectofrestructuringtheapplication

HandlingtheAjaxErrorTheotherchangeIneedtomakeistoaddsomekindoferrorhandlerforwhentheAjaxrequestfails.Thismayseemlikeabasictechnique,butmanywebapplicationsarecodedonlyforsuccess,andwhentheconnectionfails,everythingfallsapart.TherearelotsofwaysofhandlingAjaxerrors,buttheoneshowninListing5-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);

Page 134: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER5CREATINGOFFLINEWEBAPPS

134

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); }); $('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); } }); }); }); </script>

jQuerymakesiteasytohandleerrorswiththeerrormethod.ThisisanotherpartofthePromisesfeature,andthefunctionpassedtotheerrormethodwillbeexecutedifthereisaproblemwiththerequest.Inthisexample,IcreatedasimplejQueryUIdialogboxthattellstheuserthatthereisaproblem.

Page 135: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER5CREATINGOFFLINEWEBAPPS

135

AddingtheAjaxURLtotheMainManifestorFALLBACKSectionsTheworstthingyoucandoatthispointisaddtheAjaxURLtothemainsectionofthemanifest.ThebrowserwilltreattheURLlikeanyotherresource,downloadingandcachingthecontentwhenthemanifestisprocessed.WhentheclientmakestheAjaxrequest,thebrowserwillreturnthecontentfromtheapplicationcache,andthedatawon’tbeupdateduntilamanifestchangetriggersacacheupdate.Theresultofthisisthatyouruserswillbeworkingwithstaledata,whichisgenerallycontrarytothereasoningbehindmakingtheAjaxrequestinthefirstplace.

YougetprettymuchthesameresultifyouaddtheURLtotheFALLBACKsection.Everyrequest,evenwhenthebrowserisonline,willbesatisfiedbywhateveryousetasthefallback,andnorequestwilleverbemadetotheserver.

AddingtheAjaxURLtotheManifestNETWORKSectionThebestapproach(albeitfarfromideal)istoaddtheAjaxURLtotheNETWORKsectionofthemanifest.Whenthebrowserisonline,theAjaxrequestswillbepassedtotheserver,andthelatestdatawillbepresentedtotheuser.

Theproblemsstartwhenthebrowserisoffline.TherearetwodifferentapproachestohandlingAjaxrequestsinanofflinebrowser.Thefirstapproach,whichyoucanseeinGoogleChrome,isthattheAjaxrequestwillfail.YourAjaxerrorhandlerwillbeinvoked,andthereisacleanfailure.

TheotherapproachcanbeseeninFirefox.Whenthebrowserisoffline,Ajaxrequestswillbeservicedusingthemainbrowsercacheifpossible.ThiscreatestheoddsituationwheretheuserwillgetstaledataifarequestforthesameURLwasmadebeforethebrowserwentofflineandwillgetanerrorifthisisthefirsttimethattheURLhasbeenaskedfor.

UnderstandingthePOSTRequestBehaviorThewaythatPOSTrequestsarehandledisalotmoreconsistentthanforGETrequests.Ifthebrowserisonline,thenthePOSTrequestwillbemadetotheserver.Ifthebrowserisoffline,thentherequestwillfail.ThisistrueforPOSTrequeststhataremadeusingregularHTMLandforPOSTrequestsmadeusingAjax.

ThisleadstoannoyedusersbecausePOSTingaformusuallycomesaftersomeperiodofactivityontheirpart.InthecaseoftheCheeseLuxexample,theuserwillhavepagedthroughthecategoriesandenteredtheamountsofeachproducttheyrequire.Whentheycometosubmittheirorder,thebrowserwillshowanerrorpage.Youcan’tevenusetheFALLBACKsectionofthemanifesttonominateapagetobeshowninsteadoftheerror.

Theonlysensiblethingtodoistointercepttheformsubmissionandusethenavigator.onLinepropertyandeventstomonitorthebrowserstatusandpreventtheuserfromtryingtopostcontentwhenthebrowserisoffline.InChapter6,I’llshowyousometechniquesforpreservingtheresultoftheuser’seffort,readyforwhenthebrowsercomesbackonline.

Page 136: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER5CREATINGOFFLINEWEBAPPS

136

SummaryInthischapter,IshowedyouhowtousetheHTML5ApplicationCachetocreateofflineapplications.Byusingtheapplicationcache,youcancreateapplicationsthatareavailableevenwhentheuserdoesn’thaveanetworkconnection.Althoughthecoreoftheapplicationcacheiswell-supported,therearesomeanomalies,andcarefuldesignandtestingarerequiredtogetaresultthatisreliableandrobust.Inthenextchapter,I’llshowyouhowtousesomerelatedfunctionalitythathelpssmoothoutsomeoftheroughedgesofofflineappsandthatcanbeusedtocreateabetterexperiencefortheuser.

Page 137: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

C H A P T E R 6

137

Storing Data in the Browser

Anaturalcomplementtoofflineapplicationsisclient-sidedatastorage.HTML5definessomeusefulJavaScriptAPIsforstoringdatainthebrowser,rangingfromsimplename/valuepairstousingaJavaScriptobjectdatabase.Inthischapter,Ishowyouhowtobuildapplicationsthatrelyonpersistentlystoreddata,includingdetailsofhowtousesuchdatainanofflinewebapplication.

CautionThebrowsersupportfordatastorageismixed.YoushouldruntheexamplesinthischapterusingGoogleChrome,withtheexceptionofthoseintheIndexedDBsection,whichwillrunonlyinMozillaFirefox.

UsingLocalStorageThesimplestwaytostoredatainthebrowseristousetheHTML5localstoragefeature.Thisallowsyoutostoresimplename/valuepairsandretrieveormodifythemlater.Thedataisstoredpersistentlybutisnotguaranteedtobestoredforever.Thebrowserisfreetodeleteyourdataifitneedsthespace(orifthedatahasn’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 = {

Page 138: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

138

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,whichallowsmetofocusonthestoragetechniqueswithoutthefeaturesfromotherchaptersgettingintheway.Asthelistingshows,gettingstartedwithlocalstorageisprettysimple.ThegloballocalStorageobjectactslikeanarray.Whentheusermakesaselectioninthissimplewebapp,Istoretheselecteditemusingarray-stylenotation,likethis:

localStorage["selection"] = item;

TipKeysarecase-sensitive(sothatselectionandSelectionwouldrepresentdifferentdataitems),andassigningavaluetoakeythatalreadyexistsoverwritesthepreviouslydefinedvalue.

Page 139: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

139

Thisstatementcreatesanewlocalstorageitem,whichIcanreadbackusingthesamearray-stylenotation,likethis:

viewModel.selectedItem(localStorage["selection"] || viewModel.items[0]);

Theeffectofaddingthesetwostatementstotheexampleistocreatesimplepersistencefortheuser’sselection.Whenthewebappisloaded,Ichecktoseewhetherthereisdatastoredundertheselectionkeyand,ifthereis,setthecorrespondingdataitemintheviewmodel,whichrestorestheuser’sselectionfromanearliersession.

TipItisimportantnottouselocalstorageforsensitiveinformationortotrusttheintegrityofdataretrievedfromlocalstorageforcriticalfunctionsinyourwebapp.Userscanseeandeditthecontentsoflocalstorage,whichmeansthatnothingyoustoreissecretandeverythingcanbechanged.Don’tstoreanythingyoudon’twantpublicallydisseminated,anddon’trelyonlocalstoragetogiveprivilegedaccesstoyourwebapp.

Fromthatpointon,IupdatethevalueassociatedwiththeselectionkeyeachtimemyrouteismatchedbyaURLchange.Iincludedafallbacktoadefaultselectiontocopewiththepossibilitythatthelocalstoragedatahasbeendeleted(orthisisthefirsttimethattheuserhasloadedthewebapp).Totestthisfeature,loadtheexamplewebapp,selectoneoftheoptions,andthenreloadthewebpage.Thebrowserwillreloadthedocument,executetheJavaScriptcodeafresh,andrestoreyourselection.

StoringJSONDataThespecificationforlocalstoragerequiresthatkeysandvaluesarestrings,justlikeinthepreviousexample.Beingabletostorealistofname/valuepairsisn’talwaysthatuseful,butwecanbuildonthesupportforstringstouselocalstorageforJSONdata,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"); } }

Page 140: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

140

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.ThestoreViewModelDatafunctioniscalledwhenevertheusermakesaselection.JSONisonlyabletostoredatavaluesandnotJavaScriptfunctions,soIextractthedatavaluesfromtheviewmodelandusethemtocreateanewobject.IpassthisobjecttotheJSON.stringifymethod,whichreturnsaJSONstring,likethis:

{"items":["Apple","Orange","Banana"], "selectedItem":"Banana"}

IstorethisstringbyassociatingitwiththeviewModelDatakeyinlocalstorage.ThecorrespondingfunctionisloadViewModelData.IcallthisfunctionwhenthejQueryreadyeventisfiredanduseittocompletetheviewmodel.

TipThepersistentnatureoflocalstoragemeansthatifyoureuseakeytostoreadifferentkindofdata,youruntheriskofencounteringtheoldformatthatwasstoredinaprevioussession.Thesimplestwaytohandlethisindevelopmentistoclearthebrowser’scache.Inproduction,youmustbeabletodetecttheolddataandeitherprocessitor,attheveryleast,beabletodiscarditwithoutgeneratinganyerrors.

Page 141: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

141

IloadtheJSONstringandusetheJSON.parsemethodtocreateaJavaScriptobjectifthereislocalstoragedataassociatedwiththeviewModelDatakey.Icanthenreadthepropertiesoftheobjecttopopulatetheviewmodel.Ofcourse,Icannotrelyontherebeingdataavailable,soIfallbacktosomesensibledefaultvaluesifneeded.

STORING OBJECT DATA

Itwasn’thardtoseparatethedatafromtheobjectthatcontaineditinmysimpleexample,butitcanbesignificantlymoredifficultinacomplexwebapplication.Youmightbetemptedtoshortcutthisprocessbystoringobjectsdirectly,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> ...

Page 142: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

142

Thistechniquedoesn’twork.Thebrowserwon’tcomplainwhenyoustoreobjects,andifyoureadthevaluebackwithinthesamesession,everythinglooksfine.Butthebrowserserializestheobjectinordertostoreitforfuturesessions.FormostJavaScriptobjects,thestoredvaluewillbe[object Object],whichistheresultyougetifyoucallthetoStringmethod.Whentheuserrevisitsthewebapp,thevalueinlocalstorageisn’tavalidJavaScriptobjectandcan’tbeparsed.Thisisthekindofproblemthatshouldbedetectedduringtesting,butIseethisissuealot,notleastbecauseevenprojectsthattaketestingseriouslydon’tgenerallyrevisittheapplicationformultiplesessions.

StoringFormDataLocalstorageisideallysuitedformakingformdatapersistent.Thekey/valuemappingsuitsthenatureofformelementsverywell,andwithverylittleeffort,youcancreateformsthatarepersistentbetweensessions,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>

Page 143: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

143

<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.Whentheuserentersavalueintooneoftheinputelements,thevaluedatabindingupdatesthecorrespondingobservabledataitemintheviewmodel.Iusethesubscribefunctiontoreceivenotificationsofthesechangesandwritetheupdatetolocalstorage,likethis:

$.each(viewModel.personalDetails, function(index, item) { item.value(localStorage[item.name] || ""); item.value.subscribe(function(newValue) { localStorage[item.name] = newValue; }); });

Page 144: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

144

Isetupthesubscriptionbyenumeratingthroughtheitemsintheviewmodel.Iusethisopportunitytosettheinitialvaluesintheviewmodelfromlocalstorageifthereisdataavailable,likethis:

item.value(localStorage[item.name] || "");

WhenIsettheinitialvalue,thevaluesfromlocalstoragearepropagatedthroughtheviewmodeltotheinputelements,keepingeverythingup-to-date.

Itdoesn’tmakesensetocontinuetostoretheformdataoncetheformhasbeensubmittedorwhentheuserclickstheResetbutton.WheneithertheSubmitorResetbuttonisclicked,Iremovethedatafromlocalstorage,likethis:

$('#buttonDiv input').button().click(function(e) { localStorage.clear(); });

Theclearmethodremovesallofthedatainlocalstorageforthewebapp(butnotforotherwebapps;onlytheuserorthebrowseritselfcanaffectstorageacrosswebapps).Ididnotpreventthedefaultactionforeitherbutton,whichmeansthattheformwillbesubmittedbythesubmitbutton,andtheformwillberesetbytheresetbutton.

TipStrictlyspeaking,Ineednothavehandledtheclickeventfortheresetbuttonsincetheviewmodelwouldhaveledtoemptyvaluesbeingwrittentolocalstorage.Insituationslikethese,ItendtoprefercleansingthedatatwiceinordertogetsimplerJavaScriptcode.

Theeffectofthislittlewebappisthattheformdataispersistentuntiltheusersubmitstheform.Iftheusernavigatesawayfromtheformbeforesubmittingit,thedatatheyenteredbeforenavigatingawaywillberestoredthenexttimethewebappisloaded.

SynchronizingViewModelDataBetweenDocumentsThedatainlocalstorageisstoredonaper-originbasis,meaningthateachoriginhasitsownseparatelocalstoragearea.Thismeansyoudon’thavetoworryaboutkeycollisionwithotherpeople’swebapplications.Italsomeansthatwecanusewebstoragetosynchronizeviewmodelsbetweendifferentdocumentswithinthesamedomain.

Whenusinglocalstorageinthisway,Iwanttobenotifiedwhenanotherdocumentmodifiesastoreddatavalue.Icanreceivesuchnotificationsbyhandlingthestorageevent,whichisemittedbythewindowbrowserobject.Tomakethiseventeasiertouse,Ihavecreatedanewkindofobservabledataitemthatautomaticallypersistsitselftolocalstorageandthatloadschangedvaluesinresponsetothestorageevent.Iaddedthisnewfunctionalitytotheutils.jsfile,asshowninListing6-4.

Listing6-4.CreatingaPersistentObservableDataItem

... ko.persistentObservable = function(keyName, initialValue) { var obItem = ko.observable(localStorage[keyName] || initialValue); $(window).bind("storage", function(e) {

Page 145: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

145

if (e.originalEvent.key == keyName) { obItem(e.originalEvent.newValue); } }); obItem.subscribe(function(newValue) { localStorage[keyName] = newValue; }); return obItem; } ...

Thiscodeisawrapperaroundthestandardobservabledataitem,thelocalstoragedataarray,andthestorageevent.Thefunctioniscalledwithakeynamethatreferstoadataiteminlocalstorage.Whenthefunctioniscalled,Iusethekeytocheckwhetherthereisalreadydatainlocalstorageforthespecifiedkeyand,ifthereis,settheinitialvalueoftheobservable.Ifthereisn’tadefaultvalue,IusetheinitialValuefunctionargument:

var obItem = ko.observable(localStorage[keyName] || initialValue);

IusejQuerytobindtothestorageeventonthewindowobject.jQuerynormalizesevents,wrappingtheeventobjectsemittedbyelementswithajQuery-specificsubstitute.Ineedtogettotheunderlyingeventobjectbecauseitcontainsinformationaboutthechangeinlocalstorage;IdothisthroughtheoriginalEventproperty.Whenhandlingthestorageevent,theoriginalEventpropertyreturnsaStorageEventobject,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,IusetheKOsubscribemethodsothatIcanupdatethelocalstoragevalueinresponsetochangesintheviewmodel:

obItem.subscribe(function(newValue) { localStorage[keyName] = newValue; });

Page 146: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

146

Withjustafewlinesofcode,Ihavebeenabletocreateapersistentobservabledataitemformyviewmodel.

Ihavenothadtotakeanyspecialprecautionstopreventaninfiniteloopofevent-update-subscription-eventoccurring.Therearetworeasonsforthis.First,theKOobservabledataitemthatmycodewrapsaroundissmartenoughtoissueupdatesonlywhenanupdatedvalueisdifferentfromtheexistingvalue.

Second,thebrowsertriggersthestorageeventonlyinotherdocumentsinthesameoriginandnotthedocumentinwhichthechangewasmade.Ihavealwaysthoughtthiswasslightlyodd,butitdoesmeanthatmycodeissimplerthanitwouldotherwisehavebeen.

Todemonstratemynewlypersistentdataitems,Ihavedefinedanewdocumentcalledembedded.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>

Page 147: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

147

Thisdocumentduplicatestheinputelementsfromthemainexample,butwithouttheformandbuttonelements.Itdoes,however,haveaviewmodelthatusesthepersistentObservabledataitem,meaningthatchangestotheinputelementvaluesinthisdocumentwillbereflectedinlocalstorageand,equally,thatchangesinlocalstoragewillbereflectedintheinputelements.Ihavenotsupplieddefaultvaluesforthepersistentobservableitems;ifthereisnolocalstoragevalue,thenIwanttheinitialvaluetodefaulttonull,whichIachievebynotsupplyingasecondargumenttothepersistentObservablefunction.

Allthatremainsistomodifythemaindocument.Forsimplicity,Iamembeddingonedocumentinsideanother,butlocalstorageissharedacrossanydocumentsfromthesameorigin,meaningthatthistechniquewillworkwhenthosedocumentsarewithindifferentbrowsertabsorwindows.Listing6-6showsthemodificationstoexample.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>:

Page 148: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

148

<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>

IhaveusedthesamekeysforthepersistentObservablefunctionwhendefiningtheviewmodelandaddedaniframeelementthatembedstheotherHTMLdocument.Sincebothareloadedfromthesameorigin,thebrowsersharesthesamelocalstoragebetweenthem.Changingthevalueofaninputelementinonedocumentwilltriggeracorrespondingchangeintheotherdocument,vialocalstorageandthetwoviewmodels.

CautionThebrowsersdon’tprovideanyguaranteesabouttheintegrityofadataitemifupdatesarewrittentolocalstoragefromtwodocumentssimultaneously.Itishardtocaterforthiseventuality(andIhaveneverseenithappen),butitisprudenttoassumethatdatacorruptioncanoccurifyouaresharinglocalstorage.

UsingSessionStorageThecomplementtolocalstorageissessionstorage,whichisaccessedthroughthesessionStorageobject.ThesessionStorageandlocalStorageobjectsareusedinthesamewayandemitthesamestorageevent.Thedifferenceisthatthedataisdeletedwhenthedocumentisclosedinthebrowser(morespecifically,thedataisdeletedwhenthetop-levelbrowsingcontextisdestroyed,butthat’susuallythesamething).

Themostcommonuseforsessionstorageistopreservedatawhenadocumentisreloaded.Thisisausefultechnique,althoughIhavetoadmitthatItendtouselocalstoragetoachievethesameeffectinstead.Themainbenefitofsessionstorageisperformance,sincethedataisusuallyheldinmemoryanddoesn’tneedtobewrittentodisk.Thatsaid,ifyoucareaboutthemarginalperformancegainsthatthisoffers,thenyoumayneedtoconsiderwhetherthebrowseristhebestenvironmentforyourapp.Listing6-7showshowIhaveaddedsupportforsessionpersistencetomyobservabledataiteminutils.js.

Listing6-7.DefiningaSemi-persistentObservableDataItemUsingSessionStorage

ko.persistentObservable = function(keyName, initialValue, useSession) { var storageObject = useSession ? sessionStorage : localStorage var obItem = ko.observable(storageObject[keyName] || initialValue);

Page 149: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

149

$(window).bind("storage", function(e) { if (e.originalEvent.key == keyName) { obItem(e.originalEvent.newValue); } }); obItem.subscribe(function(newValue) { storageObject[keyName] = newValue; }); return obItem; }

SincethesessionStorageandlocalStorageobjectsexposethesamefeaturesandusethesameevent,Iamabletoeasilymodifymylocalstorageobservableitemtoaddsupportforsessionstorage.Ihaveaddedanargumenttothefunctionthat,iftrue,switchestosessionstorage.Iuselocalstorageiftheargumentisnotprovidedorisfalse.Listing6-8showshowIhaveappliedsessionstoragetotwooftheobservabledataitemsintheexampleviewmodel.

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)} ] }; ...

ThevaluesoftheCityandCountryelementsarehandledusingsessionstoragewhiletheNameelementremainswithlocalstorage.Ifyouloadtheexampleintothebrowser,youwillfindthatreloadingthedocumentdoesn’tclearanyofthevaluesyouhaveentered.However,onlytheNamevalueremainsifyoucloseandreopenthedocument.

UsingLocalStoragewithOfflineWebApplicationsPartofthebenefitthatcomesfromusinglocalstorageisthatitisavailableoffline.ThismeansthatwecanuselocaldatatoaddresstheproblemsarisingfromAjaxGETrequestswhenthebrowserisoffline.Listing6-9showsthecachedCheeseLuxwebappfromthepreviouschapter,updatedtotakeadvantageoflocalstorage.

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>

Page 150: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

150

<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])')

Page 151: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

151

.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>

Page 152: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

152

<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.stringifymethodtostoreacopyoftheviewmodeldatawhentheAjaxrequestissuccessful:

$.getJSON("products.json", function(data) { cheeseModel.products = data; localStorage["jsondata"] = JSON.stringify(data); })

Iaddedtheproducts.jsonURLtotheNETWORKsectionofthemanifestforthiswebapp,soIhaveareasonableexpectationthatthedatawillbeavailableandthattheAjaxrequestwillsucceed.

If,however,therequestfails,whichwilldefinitelyhappenifthebrowserisoffline,thenItrytolocateandrestoretheserializeddatafromlocalstorage,likethis:

}).error(function() { if (localStorage["jsondata"]) { cheeseModel.products = JSON.parse(localStorage["jsondata"]); } })

Assumingtheinitialrequestworks,Iwillhaveagoodfallbackpositionifsubsequentrequestsfail.TheeffectthatthistechniquecreatesissimilartothewaythatFirefoxhandlesAjaxrequestswhenthebrowserisofflinebecauseIendupusingthelastversionofthedataIwasabletoobtainfromtheserver.

Page 153: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

153

NoticethatIhaverestructuredthecodesothattherestofthewebappsetupoccursinthecompletehandlerfunction,whichistriggeredirrespectiveoftheoutcomeoftheAjaxrequest.ThesuccessorfailureofAjaxnolongerdetermineshowIprocessedit;nowitisallaboutwhetherornotIhavedata,eitherfreshfromtheserverorrestoredfromlocalstorage.

UsingLocalStoragewithOfflineFormsImentionedinChapter5thattheonlywayofdealingwithPOSTrequestsinacachedapplicationistopreventtheuserfrominitiatingtherequestwhenthebrowserisoffline.Thisremainstrue,butyoucanimprovetheexperiencethatyoudelivertotheuserbyusinglocalstoragetocreatepersistentvalues.Todemonstratethisapproach,IfirstneedtoupdatetheenhanceViewModelfunctionintheutils.jsfiletouselocalstoragetopersisttheformvalues,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.Iwanttomaketheviewmodelquantitypropertypersistentforeachcheeseproduct,soIusethevalueoftheitemidpropertytoavoidkeycollisioninlocalstorage:

item.quantity = ko.persistentObservable(item.id + "_quantity", 0);

ThesecondpointtonoteisthatwhenIloadvaluesfromlocalstorage,Iwillbeputtingstrings,andnotnumbers,intheviewmodel.However,JavaScriptiscleverenoughtoconvertstringswhenperformingmultiplicationoperations,likethis:

return this.quantity() * this.price;

EverythingworksasIwouldlikeittowork.However,JavaScriptusesthesamesymboltodenotestringconcatenationandnumericaddition,soifIhadbeentryingtosumvaluesintheviewmodel,Iwouldhavehadtotaketheextrastepofparsingthevalue,likethis:

Page 154: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

154

return Number(this.quantity()) + someOtherValue;

UsingPersistenceintheOfflineApplicationNowthatIhavemodifiedtheviewmodel,IcanchangethemaindocumenttoimprovethewaythatIhandletheformelementwhenthebrowserisoffline.Listing6-11showsthechangestotheHTMLmarkup.

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.Ihavealsochangedthesubmitbuttonsothatitisvisibleonlywhenthebrowserisonline.Listing6-12showsthecorrespondingchangestothescriptelement.

Listing6-12.ChangestothescriptElementtoSupportOfflineForms

<script> var cheeseModel = {

Page 155: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

155

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 {

Page 156: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

156

window.applicationCache.swapCache(); window.location.reload(false); } }); } else { createDialog("Try again later"); } }); }); </script>

Thisisasimplechange,andyou’llquicklyrealizethatIamdoingsomemildmisdirection.Whenthebrowserisonline,theusercansubmittheformasnormal,andanydatainlocalstorageiscleared.ThemisdirectioncomeswhenthebrowserisofflineandtheuserclickstheSaveforLaterbutton.AllIdoiscallthecreateDialogfunction,tellingtheuserthattheformdatahasbeensaved.However,Idon’tactuallyneedtosavethedatabecauseIamusingpersistentobservabledataitemsintheviewmodel.Theuserdoesn’tneedtoknowaboutthis;theyjustgetthebenefitofthepersistenceandaclearsignalfromthewebapplicationthattheformdatahasnotbeensubmitted.Whenthebrowserisonlineagain,theusercansubmitthedata.Usinglocalstorageallofthetimemeansthattheuserwon’tlosetheirdataiftheycloseandlaterreloadtheapplicationbeforebeingabletosubmittheformtotheserver.Forcompleteness,Listing6-13showsthecreateDialogfunction,whichIdefinedintheutils.jsfile.ThisisthesameapproachIusedtocreateanerrordialogintheoriginalexample,andImovedthecodeintoafunctionbecauseIneededtocreatethesamekindofdialogboxatmultiplepointsintheapplication.

Listing6-13.ThecreateDialogFunction

function createDialog(message) { $('<div>' + message + '</div>').dialog({ modal: true, title: "Message", buttons: [{text: "OK", click: function() {$(this).dialog("close")}}] }); };

Ihavetakenaverysimpleanddirectapproachtodealingwithformdatawhenthebrowserisoffline,butyoucaneasilyseehowamoresophisticatedapproachcouldbecreated.Youmight,forexample,respondtotheonlineeventbypromptingtheusertosubmitthedataorevensubmititautomaticallyusingAjax.Whateverapproachyoutake,youmustensurethattheuserunderstandsandapprovesofwhatyourwebappisdoing.

StoringComplexDataStoringname/valuepairsisperfectlysuitedtostoringformdata,butforanythingmoresophisticated,suchasimpleapproachstartstobreakdown.Thereisanotherbrowserfeature,calledIndexedDB,whichyoucanusetostoreandworkwithmorecomplexdata.

Page 157: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

157

NoteIndexedDBisonlyoneoftwocompetingstandardsforstoringcomplexdatainthebrowser.TheotherisWebSQL.AsIwritethis,theW3CissupportingIndexedDB,butitisentirelypossiblethatWebSQLwillmakeacomebackor,atleast,becomeadefactostandard.IhavenotincludedWebSQLinthischapterbecausesupportforitislimitedatpresent,butthisisanareaoffunctionalitythatisfarfromsettled,andyoushouldreviewthesupportforbothstandardsbeforeadoptingoneofthemforyourprojects.

ItisstillearlydaysforIndexedDB,andasIwritethis,thefunctionalityisavailableonlythroughvendor-specifiedprefixes,signifyingthatthebrowserimplementationsarestillexperimentalandmaydeviatefromtheW3Cspecification.Currently,thebrowserthatadheresmostcloselytotheW3CspecificationisMozillaFirefox,sothisisthebrowserIhaveusedtodemonstrateIndexedDB.

CautionTheexamplesinthischaptermaynotworkwithbrowsersotherthanFirefox.Infact,theymaynotworkevenwithversionsofFirefoxotherthantheoneIusedinthischapter(version10).Thatsaid,youshouldstillbeabletogetasolidunderstandingofhowIndexedDBworks,evenifthespecificationorimplementationschange.

TheIndexedDBfeatureisorganizedarounddatabasesthat,likelocalandsessionstorage,areisolatedonaper-originbasissothattheycanbesharedbetweenapplicationsfromthesameorigin.IndexedDBdoesn’tfollowtheSQL-basedtablestructurethatiscommoninrelationaldatabases.AnIndexedDBdatabaseismadeupofobjectstores,whichcancontainJavaScriptobjects.YoucanaddJavaScriptobjectstoobjectstores,andyoucanquerythosestoresindifferentways,someofwhichIdemonstrateshortly.

TheresultofthisapproachisastoragemechanismthatismoreinkeepingwiththestyleoftheJavaScriptlanguagebutthatendsupbeingslightlyawkwardtouse.AlmostalloperationsinIndexedDBareperformedasasynchronousrequeststowhichfunctionscanbeattachedsothattheyareexecutedwhentheoperationcompletes.TodemonstratehowIndexedDBworks,IamgoingtocreateaCheeseFinderapplication.IwillputthecheeseproductdataintoanIndexedDBdatabaseandprovidetheuserwithsomedifferentwaysofsearchingthedataforcheesestheymightlike.Figure6-2showsthefinishedwebapptohelpprovidesomecontextforthecodethatfollows.

Page 158: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

158

Figure6-2.UsingIndexedDBtoqueryproductdata

Thefigureshowstheoptiontosearchthedescriptionofeachproductinuse.Ihavesearchedforthetermcow,andthoseproductswhosedescriptionscontainthistermarelistedatthebottomofthepage.(Thereareseveralmatchesbecausemanyofthedescriptionsexplainthatthecheeseismadefromcows’milk.)

CreatingtheIndexedDBDatabaseandObjectStoreThecodeforthisexampleissplitbetweentheutils.jsfileandthemainexample.htmldocument.I’llbejumpingbetweenthesefilestodemonstratethecorefeaturesthatIndexedDBoffers.Tobegin,IhavedefinedaDBOobjectandthesetupDatabasefunctioninutils.js,asshowninListing6-14.

Page 159: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

159

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,itdefinestheversionofthedatabasethatIamexpectingtoworkwith.EachtimeImakeachangetothedatabaseschema,IincrementthevalueofthedbVersionproperty,andasyoucansee,ittookme31changesuntilIgottheresultIwantedforthisexample.ThiswaslargelybecauseofthedifferencesbetweenthecurrentdraftofthespecificationandtheimplementationinFirefox.

TipTheversionnumberisanimportantmechanisminensuringIamworkingwiththerightversionoftheschemaformyapp.I’llshowyouhowtochecktheschemaversionand,ifneeded,upgradetheschema,shortly.

Page 160: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

160

InthesetupDatabasefunction,IbeginbylocatingtheobjectthatactsasthegatewaytotheIndexedDBdatabases,likethis:

var indexDB = window.indexedDB || window.mozIndexedDB;

TheIndexedDBfeatureisavailableinFirefoxonlythroughthewindow.mozIndexedDBobjectatthemoment,butthatwillchangetowindow.indexedDBoncetheimplementationconvergesonthefinalspecification.Togiveyouthegreatestchanceofmakingtheexamplesinthispartofthechapterwork,Itrytousethe“official”IndexedDBobjectfirstandfallbacktothevendor-prefixedalternativeifitisn’tavailable.Thenextstepistoopenthedatabase:

var req = indexDB.open("CheeseDB", DBO.dbVersion);

Thetwoargumentsarethenameofthedatabaseandtheexpectedschemaversion.IndexedDBwillopenthespecifieddatabaseifitalreadyexistsandcreateitifitdoesn’t.Theresultfromtheopenmethodisanobjectthatrepresentstherequesttoopenthedatabase.TogetanythingdoneinIndexedDB,youmustsupplyhandlerfunctionsforoneormoreofthepossibleoutcomesfromarequest.

RespondingtotheUpgrade-NeededOutcomeIcareabouttwopossibleoutcomeswhenIopenthedatabase.First,Iwanttobenotifiedifthedatabasealreadyexistsandtheschemaversiondoesn’tmatchtheversionIamexpecting.Whenthishappens,Iwanttodeletetheobjectstoresinthedatabaseandstartover.Ireceivenotificationofaschemamismatchbyregisteringafunctionthroughtheonupgradeneededproperty:

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.IgetalistoftheexistingobjectstoresthroughtheobjectStoreNamespropertyanddeleteeachinturnusingthedeleteObjectStoremethod.Indeletingtheobjectstores,Ialsodeletethedatatheycontain.Thisisfineforsuchasimplewebappwhereallofthedataiscomingfromtheserverandiseasilyreplaced,butyoumayneedtotakeamoresophisticatedapproachifyourdatabasescontaindatathathasbeengeneratedasaresultofuseractions.

Page 161: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

161

CautionThefunctionassignedtotheonupgradeneededpropertyistheonlyopportunityyouhavetomodifytheschemaofthedatabase.Ifyoutrytoaddordeleteanobjectstoreelsewhere,thebrowserwillgenerateanerror.

Oncetheexistingobjectstoresareoutoftheway,IcancreatesomenewonesusingthecreateObjectstoremethod.Theargumentstothismethodarethenameofthenewstoreandanoptionalobjectcontainingconfigurationsettingstobeappliedtothenewstore.IhaveusedthekeyPathconfigurationoption,whichletsmesetadefaultkeyforobjectsthatareaddedtothestore.Ihavespecifiedtheidpropertyasthekey.IhavealsocreatedanindexusingthecreateIndexmethodonthenewlycreatedobjectstore.Anindexallowsmetoperformsearchesintheobjectstoreusingapropertyotherthanthekey,inthiscase,thecategoryproperty.I’llshowyouhowtouseanindexshortly.

Finally,Iaddobjectstothedatastore.WhenIusethisfunctioninthemaindocument,I’llbeusingthedataIgetfromanAjaxrequestfortheproducts.jsonfile.ThisisinthesameformatasthedataIhavebeenusingthroughoutthisbook.IusethejQueryeachfunctiontoenumerateeachcategoryandtheitemsitcontains.IhaveaddedacategorypropertytoeachitemsothatIcanfindalloftheproductsthatbelongtothesamecategorymoreeasily.

TipTheobjectsyouaddtoanobjectstoreareclonedusingtheHTML5structuredclonetechnique.ThisisamorecomprehensiveserializationtechniquethanJSON,andthebrowserwillgenerallymanagetodealwithcomplexobjects,justaslongasnoneofthepropertiesisafunctionorDOMAPIobject.

RespondingtotheSuccessOutcomeThesecondoutcomeIcareaboutissuccess,whichIhandlebyassigningafunctiontotheonsuccesspropertyoftherequesttoopenthedatabase,asfollows:

req.onsuccess = function(e) { DBO.db = this.result; callback(); };

ThefirststatementinthisfunctionassignstheopeneddatabasetothedbpropertyoftheDBOobject.ThisisjustaconvenientwaytokeepahandleonthedatabasesothatIcanuseitinotherfunctions,somethingthatI’lldemonstrateshortly.

ThesecondstatementinvokesthecallbackfunctionthatwaspassedasthesecondargumenttothesetupDatabasefunction.Itisn’tsafetoassumethatthedatabaseisopenuntiltheonsuccessfunctionisexecuted,whichmeansIneedtohavesomemechanismforsignalingthefunctioncallerthatthedatabasehasbeensuccessfullyopenedanddata-relatedoperationscanbestarted.

Page 162: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

162

TipIndexedDBrequestshaveacounterpartoutcomepropertycalledonerror.Iwon’tbedoinganyerrorhandlingintheseexamplesbecause,asIwritethis,tryingtodealwithIndexedDBerrorscausesmoreproblemsthanitsolves.Ideally,thiswillhaveimprovedbythetimeyoureadthischapter,andyouwillbeabletowritemorerobustcode.

IncorporatingtheDatabaseintotheWebApplicationListing6-15showsthemarkupandinlineJavaScriptfortheexampleapplication.Withtheexceptionofthedatabase-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); } }

Page 163: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

163

} $.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>

Page 164: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

164

</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,IhaveusedaviewmodeltobindthestateoftheapplicationtotheHTMLmarkup.Mostofthedocumentistakenupdefiningandcontrollingtheviewgiventotheuserandsupportinguserinteractions.

Whentheuserclicksthesearchbutton,oneofthreefunctionsintheutils.jsfileiscalled,dependingontheselectedsearchmode.IftheuserhaselectedtosearchbyproductID,thenthegetProductByIDfunctioniscalled.ThegetProductsByDescriptionfunctionisusedwhentheuserwantstosearchtheproductdescriptions,andthegetProductsByCategoryfunctionisusedtofindalltheproductsinaspecificcategory.Eachofthesefunctionstakestwoarguments:thetexttosearchforandacallbackfunctiontowhichtheresultsshouldbedispatched(evensearchinganobjectstoreisanasynchronousoperationwithIndexedDB).Thecallbackfunctionisthesameforallthreesearchmodes:handleSearchResults.Theresultfromthesearchfunctionswillbeasingleproductobjectoranarrayofobjects.ThejobofthehandleSearchResultsfunctionistoclearthecontentsoftheselectedItems

Page 165: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

165

observablearrayintheviewmodelandreplacethemwiththenewresults;thiscausestheelementstobeupdatedandtheresultstobedisplayedtotheuser.

NoticethatIplacemostofthecodestatementsinmyinlinescriptelementinsidethecallbackforthesetupDatabasefunction.Thisisthefunctionthatiscalledwhenthedatabasehassuccessfullybeenopened.

LocatinganObjectbyKeyThefirstofthesearchfunctionsisgetProductByID,whichlocatesanobjectbasedonthevalueoftheidproperty.YouwillrecallthatIspecifiedthispropertyasthekeyfortheobjectstorewhenIcreatedthedatabase:

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,youmustcreateatransaction,usingthetransactionmethod,declaringtheobjectsstoresthatyouwanttoworkwith.Onlythencanyouopenanobjectstore,usingtheobjectStoremethodonthetransactionyoujustcreated.

TipYoudon’tneedtoexplicitlycloseyourobjectstoreoryourtransactions;thebrowserclosesthemforyouwhentheyareoutofscope.Thereisnobenefitintryingtoexplicitlyforcethestoreortransactionstoclose.

Iobtaintheobjectwiththespecifiedkeyusingthegetmethod,whichmatchesatmostoneobject(iftherearemultipleobjectswiththesamekey,thenthefirstmatchingobjectismatched).Themethodreturnsarequest,andImustsupplyafunctionfortheonsuccesspropertytobenotifiedwhenthesearchhascompleted.Thematchedobjectisavailableintheresultpropertyoftherequest,whichIpassbacktothemainpartofthewebappbyinvokingthecallbackfunctionpassedtothegetProductByIDfunction(which,asyouwillrecall,isthehandleSearchResultsfunction).

The(eventual)resultfromthegetmethodisaJavaScriptobjector,ifthereisnomatch,null.Idon’thavetoworryaboutre-creatinganobjectfromtheserializeddatastoredbythedatabaseoruseanykindofobject-relationalmappinglayer.TheIndexedDBdatabaseworksonJavaScriptobjectsthroughout,whichisanicefeature.

Page 166: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

166

Itisalittlefrustratingtohavetousecallbackseverytimeyouwanttoperformasimpleoperation,butitquicklybecomessecondnature.TheresultisastoragemechanismthatfitsnicelyintotheJavaScriptworldandthatdoesn’ttieupthemainthreadofexecutionwhenlongoperationsarebeingperformedbutthatrequirescarefulthoughtandapplicationdesigntobeproperlyused.

LocatingObjectsUsingaCursorIhavetotakeadifferentapproachwhentheuserwantstosearchforproductsbytheirdescription.Descriptionsarenotakeyinmyobjectstore,andIwanttobeabletolookforpartialmatches(otherwisetheuserwouldhavetoexactlytypeinallofthedescriptiontomakeamatch).Listing6-17showsthegetProductsByDescriptionfunction,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); } }; };

Mytechniquehereistouseacursortoenumeratealloftheobjectsintheobjectstoreandlookforthosewhoseproductspropertycontainsthesearchtermprovidedbytheuser.AcursorsimplykeepstrackofmyprogressasIenumeratethroughasequenceofdatabaseobjects.

IndexedDBdoesn’thaveatextsearchfacility,soIhavetohandlethismyself.CallingtheopenCursormethodonanobjectstorecreatesarequestwhoseonsuccesscallbackisexecutedwhenthecursorisopened.Thecursoritselfisavailablethroughtheresultpropertyofthethiscontextobject.(Itshouldalsobeavailablethroughtheresultpropertyoftheeventpassedtothefunction,butthecurrentimplementationdoesn’talwayssetthisreliably.)

Ifthecursorisn’tnull,thenthereisanobjectavailableinthevalueproperty.IchecktoseewhetherthedescriptionpropertyoftheobjectcontainsthetermIamlookingfor,andifitdoes,Ipushtheobjectintoalocalarray.Tomovethecursortothenextobject,Icallthecontinuemethod,whichexecutestheonsuccessfunctionagain.

ThecursorisnullwhenIhavereadalloftheobjectsintheobjectstore.Atthispoint,mylocalarraycontainsalloftheobjectsthatmatchmysearch,andIpassthembacktothemainpartofthewebapplicationusingthecallbacksuppliedasthesecondargumenttothegetProductsByDescriptionfunction.

Page 167: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

167

LocatingObjectsUsinganIndexEnumeratingalloftheobjectsinanobjectstoreisn’tanefficientwayoffindingobjects,whichiswhyIcreatedanindexforthecategorypropertywhenIsetuptheobjectstore:

objectStore.createIndex("category", "category", {unique: false});

TheargumentstothecreateIndexmethodarethenameoftheindex,thepropertyintheobjectsthatwillbeindexed,andaconfigurationobject,whichIhaveusedtotellIndexedDBthatthevaluesforthecategorypropertyarenotunique.

ThegetProductsByCategoryfunction,whichisshowninListing6-18,usestheindextonarrowtheobjectsthatareenumeratedbythecursor.

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); } }; };

TheIDBKeyRangeobjecthasanumberofmethodsforconstrainingthekeyvaluesthatwillmatchobjectsintheobjectstore.IhaveusedtheonlymethodtospecifythatIwantexactmatchesonly.

IopentheindexbycallingtheindexmethodontheobjectstoreandpassintheIDBKeyRangeobjectasanargumentwhenIopenthecursor.Thishastheeffectofnarrowingthesetofobjectsthatareavailablethroughthecursor,meaningthattheresultsIpassviathecallbackcontainonlythecheeseproductsinthespecifiedcategory.Thereisnopartialmatchinginthisexample;theusermustentertheentirecategoryname,suchasFrenchCheese.

SummaryInthischapter,Ishowedyouhowtouselocalstoragetopersistentlystorename/valuepairsinthebrowserandhowthisfeaturecanbeusedinanofflinewebapptodealwithHTMLforms.IalsoshowedyoutheIndexedDBfeatures,whichisfarlessmaturebutshowspromiseasafoundationforstoringandqueryingmorecomplexdatausingnaturalJavaScriptobjectsandlanguageidioms.

IndexedDBisn’tyetreadyforproductionuse,butIfindthatlocalstorageisveryrobustandhelpfulinawiderangeofsituations.Ifinditespeciallyusefulinmakingformsmoreusefulandlessannoying,muchasIdemonstratedinthischapter.Thelocalstoragefeatureisveryeasytouse,especiallywhenitisembeddedwithinyourapplicationviewmodel.

Page 168: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER6STORINGDATAINTHEBROWSER

168

Inthenextchapter,Ishowyouhowtocreateresponsivewebappsthatadaptandrespondtothecapabilitiesofthedevicesonwhichtheyrun.

Page 169: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

C H A P T E R 7

169

Creating Responsive Web Apps

Therearetwoapproachestotargetingmultipleplatformswithawebapp.Thefirstistocreateadifferentversionoftheappforeachkindofdeviceyouwanttotarget:desktop,smartphone,tablet,andsoon.I’llgiveyousomeexamplesofhowtodothisinChapter8.

Theotherapproach,andthetopicofthischapter,istocreatearesponsivewebapp,whichsimplymeansthatthewebappadaptstothecapabilitiesofthedeviceitisrunningon.Ilikethisapproachbecauseitdoesn’tdrawaharddistinctionbetweenmobileand“normal”devices.

Thisisimportantbecausethecapabilitiesofsmartphones,tablets,anddesktopsblurtogether.ManymobilebrowsersalreadyhavegoodHTML5support,anddesktopmachineswithtouchscreensarebecomingmorecommon.Inthischapter,I’llshowyoutechniquesthatyoucanusetocreatewebapplicationsthatareflexibleandfluid.

SettingtheViewportIneedtoaddressoneissuethatisspecifictothebrowsersrunningonsmartphonesandtablets(whichI’llstartreferringtoasmobilebrowsers).Mobilebrowserstypicallystartfromtheassumptionthatawebsitewillhavebeendesignedforalarge-screeneddesktopdeviceandthat,asaconsequence,theuserwillneedsomehelptobeabletoviewit.Thisisdonethroughtheviewport,whichscalesdownthewebpagesothattheusergetsasenseoftheoverallpagestructure.Theuserthenzoomsintoaparticularregionofthepageinordertoreadoruseit.YoucanseetheeffectinFigure7-1.

Figure7-1.Theeffectofthedefaultviewportinamobilebrowser

Page 170: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER7CREATINGRESPONSIVEWEBAPPS

170

NoteThescreenshotsinFigure7-1areoftheOperaMobileemulator,whichyoucangetfromwww.opera.com/developer/tools/mobile.Althoughithassomequirks,thisemulatorisreasonablyfaithfultotherealOperaMobile,whichiswidelyusedinmobiledevices.Ilikeitbecauseitallowsmetocreateemulatorswithscreensizesrangingfromsmallsmartphonestolargetabletsandtoselectwhethertoucheventsaresupported.Asabonus,youcandebugandinspectyourwebappusingthestandardOperadevelopmenttools.Anemulatorisnosubstitutefortestingonarangeofrealhardwaredevicesbutcanbeveryconvenientduringtheearlystagesofdevelopment.

Thisisasensiblefeature,butyouneedtodisableitforwebapps;otherwise,contentandcontrolsaredisplayedatasizethatistoosmalltouse.Listing7-1showshowtodisablethisfeatureusingtheHTMLmetatag,whichIhaveappliedtoasimplifiedversionoftheCheeseLuxwebapp,whichwillbethefoundationexampleforthischapter.

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);

Page 171: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER7CREATINGRESPONSIVEWEBAPPS

171

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>

Page 172: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER7CREATINGRESPONSIVEWEBAPPS

172

</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.YoucanseetheeffectinFigure7-2.ThisparticularmetatagtellsthebrowsertodisplaytheHTMLdocumentusingtheactualwidthofthedisplayandwithoutanymagnification.Ofcourse,thewebappisstillamess,butitisamessthatisbeingdisplayedatthecorrectsize,whichisthefirststeptowardaresponsiveapp.Intherestofthischapter,I’llshowyouhowtorespondtodifferentdevicecharacteristicsandcapabilities.

Figure7-2.Theeffectofdisablingtheviewportforawebapp

Page 173: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER7CREATINGRESPONSIVEWEBAPPS

173

RespondingtoScreenSizeMediaqueriesareausefulwayoftailoringCSSstylestothecapabilitiesofthedevice.Perhapsthemostimportantcharacteristicofadevicefromtheperspectiveofaresponsivewebappisscreensize,whichCSSmediaqueriesaddressverywell.AsFigure7-2shows,theCheeseLuxlogotakesupalotofspaceonasmallscreen,andIcanuseaCSSmediaquerytoensurethatitisshownonlyonlargerdisplays.Listing7-2showsasimplemediaquerythatIaddedtothestyles.cssfile.

Listing7-2.ASimpleMediaQuery

@media screen AND (max-width:500px) { *.largeScreenOnly { display: none; } }

TipOperaMobileaggressivelycachesCSSandJavaScriptfiles.Whenexperimentingwithmediaqueries,thebesttechniqueistodefinetheCSSandscriptcodeinthemainHTMLdocumentandmoveittoexternalfileswhenyouarehappywiththeresult.Otherwise,youwillneedtoclearthecache(orrestarttheemulator)toensureyourchangesareapplied.

The@mediatagtellsthebrowserthatthisisamediaquery.IhavespecifiedthatthelargeScreenOnlystylecontainedinthisqueryshouldbeappliedonlyifthedeviceisascreen(asopposedtoaprojectororprintedmaterial)andthewidthisnogreaterthan500pixels.

TipInthischapter,Iamgoingtodividetheworldintotwocategoriesofdisplays.Smalldisplayswillbethosewhosewidthisnogreaterthan500pixels,andlargedisplayswillbeeverythingelse.Thisissimpleandarbitrary,andyoumayneedtodevisemorecategoriestogettheeffectyourequireforyourwebapp.Iamgoingtoignoretheheightofthedisplayentirely.Mysimplecategorieswillkeeptheexamplesinthischaptermanageable,albeitatthecostofgranularity.

Iftheseconditionsaremet,thenastyleisdefinedthatsetstheCSSdisplaypropertyforanyelementassignedtothelargeScreenOnlyclasstonone,whichhidestheelementfromview.Withtheadditiontothestylesheet,IcanensurethattheCheeseLuxlogoisshownonlyonlargedisplaysbyapplyingthelargeScreenOnlyclasstomymarkup,asshowninListing7-3.

Page 174: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER7CREATINGRESPONSIVEWEBAPPS

174

Listing7-3.UsingCSSMediaQueriestoRespondtoScreenSizes

... <div id="logobar" class="largeScreenOnly"> <img src="cheeselux.png"> <span id="tagline">Gourmet European Cheese</span> </div> ...

CSSmediaqueriesarelive,whichmeansthecategoryofscreensizecanchangeifthebrowserwindowisresized.Thisisn’tmuchuseonmobiledevices,butitmeansthataresponsivewebappwilladapttothedisplaysizeevenonadesktopplatform.YoucanseehowthelayoutsalterinFigure7-3.

Figure7-3.Usingmediaqueriestomanagethevisibilityofelements

UsingMediaQuerieswithJavaScriptToproperlyintegratemediaqueriesintoawebapp,weneedtousetheViewmoduleoftheW3CCSSObjectModelspecification,whichbringsJavaScriptmediaqueriessupportintothebrowser.MediaqueriesareevaluatedinJavaScriptusingthewindow.matchMediamethod,asshowninListing7-4.IhavedefinedthedetectDeviceFeaturesfunctionintheutils.jsfile;atthemoment,itdetectsonlythescreensize,butI’lldetectsomeadditionalfeatureslater.Thereisalotgoingoninthelisting,soI’llbreakitdownandexplainthevariouspartsinthesectionsthatfollow.

Listing7-4.UsingaMediaQueryinJavaScript

function detectDeviceFeatures(callback) { var deviceConfig = {}; Modernizr.load({ test: window.matchMedia, nope: 'matchMedia.js', complete: function() {

Page 175: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER7CREATINGRESPONSIVEWEBAPPS

175

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); } }); };

LoadingthePolyfillIneedtouseapolyfilltomakesureIcanusethematchMediamethod.Supportforthisfeatureisgoodindesktopbrowsersbutspottyinthemobileworld.ThepolyfillIuseiscalledmatchMedia.jsandisavailablefromhttp://github.com/paulirish/matchMedia.js.

Iwanttoloadthepolyfillonlyifthebrowserdoesn’tsupportthematchMediafeaturenatively.Toarrangethis,IhaveusedtheModernizr.loadmethod,whichisaflexibleresourceloader.IpasstheloadmethodanobjectwhosepropertiestellModernizrwhattodo.

TipTheModernizr.loadfeatureisavailableonlywhenyoucreateacustomModernizrbuild;itisnotincludedintheuncompresseddevelopmentversionoftheModernizrlibrary.TheModernizrloadmethodisawrapperaroundalibrarycalledYepNope,whichisavailableathttp://yepnopejs.com.YoucanuseYepNopedirectlyifyoudon’twanttouseacompressedModernizrbuildforanyreason.Thehttp://yepnopejs.comsitealsocontainsdetailsofalloftheloaderfeatures;thesyntaxdoesn’tchangewhenthelibraryisincludedwithModernizr.BecarefulwhenusingaresourceloaderinexternalJavaScriptfiles.Thereareseriousissuesthatcanarise,whichIdescribeinChapter9.YouwillseealinktocreateacustomdownloadontheModernizrwebpage.ForthecustombuildthatIusedinthischapter,IsimplycheckedalloftheoptionstoincludeasmuchModernizrfunctionalityaspossibleinthedownload.

Thetestproperty,asthenamesuggests,specifiestheexpressionthatIwantModernizrtoevaluate.Inthiscase,Iwanttoseewhetherthewindow.matchMediamethodisdefinedbythebrowser.YoucanuseanyJavaScriptexpressionwiththetestproperty,includingModernizrfeaturedetectionchecks.

Page 176: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER7CREATINGRESPONSIVEWEBAPPS

176

ThenopepropertytellsModernizrwhatresourcesIwanttoloadiftestevaluatesfalse.Inthisexample,IhavespecifiedthematchMedia.jsfile,whichcontainsthepolyfillcode.Thereisacorrespondingproperty,yep,whichtellsModernizrwhatresourcesarerequirediftestistrue,butIdon’tneedtousethatinthisexamplebecauseIwillberelyingonthebuilt-insupportformatchMediaiftestistrue.Thecompletepropertyspecifiesafunctionthatwillbeexecutedwhentheresourcesspecifiedbytheyepornopepropertyhaveallbeenloadedandexecuted.

Modernizr.loadgetsandexecutesJavaScriptscriptsasynchronously,whichiswhythedetectDeviceFeaturesfunctiontakesacallbackfunctionasanargument.Iinvokethiscallbackattheendofthecompletefunction,passinginanobjectthatcontainsdetailsofthefeaturesthathavebeendetected.

DetectingtheScreenSizeIcannowturntoworkingoutwhetherthedevice’sscreenfallsintomylargeorsmallcategory.Todothis,Ipassamediaquery,justtheliketheoneIusedinCSS,tothematchMediamethod,likethis:

var screenQuery = window.matchMedia('screen AND (max-width:500px)');

IdeterminewhethermymediaqueryhasbeenmatchedbyreadingthematchespropertyoftheobjectIgetbackfrommatchMedia.Ifmatchesistrue,thenIamdealingwithascreenthatisinmysmallcategory(500pixelsandsmaller).Ifitisfalse,thenIhavealargescreen.IassigntheresulttoanobservabledataitemintheobjectthatIpasstothecallbackfunction:

var deviceConfig = { smallScreen: ko.observable(screenQuery.matches) };

IfthebrowserimplementsthematchMediafeature,thenIcanusetheaddListenermethodtobenotifiedwhenthestatusofthemediaquerychanges,likethis:

if (screenQuery.addListener) { screenQuery.addListener(function(mq) { deviceConfig.smallScreen(mq.matches); }); }

Thestatusofamediaquerychangeswhenoneoftheconditionsitcontainschanges.Thetwoconditionsinmyqueryarethatweareworkingonascreenandthatithasamaximumwidthof500pixels.Achangenotification,therefore,indicatesthatthewidthofthedisplayhaschanged.Thismeansthatthebrowserwindowhasbeenresizedorthatthescreenorientationhaschanged(seethe“RespondingtoScreenOrientation”sectionlaterinthischapterformoredetails).

ThematchMedia.jspolyfilldoesn’tsupportchangenotifications,soIhavetotestfortheexistenceoftheaddListenermethodbeforeIuseit.MyfunctionisexecutedwhenthestatusofthemediaquerychangesandIupdatethevalueoftheobservabledataitem.ThelastthingIdoiscreateacomputedobservabledataitem,likethis:

deviceConfig.largeScreen = ko.computed(function() { return !deviceConfig.smallScreen(); });

ThisisjusttohelptidyupmysyntaxwhenIwanttorefertothescreensizeintherestofmywebappsothatIcanrefertosmalllScreenandlargeScreentofigureoutwhatIamworkingwith,asopposedtosmallScreenand!smallScreen.Itisasmallthing,butIcreatefewertyposthisway.

Page 177: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER7CREATINGRESPONSIVEWEBAPPS

177

Somebrowsersareinconsistentinthewaythatstatuschangesinmediaqueriesarehandled.Forexample,theversionofGoogleChromethatiscurrentasIwritethisdoesn’talwaysupdatemediaquerieswhenthescreensizechanges.Asabelt-and-bracesmeasure,Ihaveaddedasimplecheckonthescreensize,whichissetupusingthesetIntervalfunction:

setInterval(function() { deviceConfig.smallScreen(window.innerWidth <= 500); }, 500);

Thefunctionisexecutedevery500millisecondsandupdatesthescreensizeitemintheviewmodel.Thisisn’tideal,butitisimportantthataresponsivewebappisabletoadapttodevicechanges,andthiscanmeantakingsomeundesirableprecautions,includingpollingforstatuschanges.

TipNoticethatIusethewindow.innerWidthpropertytotrytofigureoutthesizeofthescreen.TheproblemIamworkingaroundisthatthemediaqueriesdon’tworkproperlyinallbrowsers,soIneedtofindasubstitutemechanismforassessingscreensize.

IntegratingCapabilityDetectionintotheWebAppIwanttodetectthecapabilitiesofthedevicebeforeIdoanythingelseinthewebapp,whichIwhyIaddedacallbacktothedetectDeviceFeaturesfunction.YoucanseehowIhaveintegratedtheuseofthisfunctiontothewebappscriptelementinListing7-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 ?

Page 178: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER7CREATINGRESPONSIVEWEBAPPS

178

newCat : cheeseModel.products[0].category); }); }); }); }); </script>

IassigntheobjectthatthedetectDeviceFeaturesfunctionpassestothecallbacktothedevicepropertyintheviewmodel.Byusinganobservabledataitem,Idisseminatechangesintotheapplicationfromtheviewmodelwhenthemediaquerychanges.

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.TheCheeseLuxlogoisvisibleonlyonlargescreens.YoumightbewonderingwhyIhavegonetoalltheeffortofre-creatingasimpleandelegantCSStechniqueinJavaScript.Thereasonissimple:pushinginformationaboutthecapabilitiesofthedevicethroughmywebappviewmodelgivesmeafoundationforcreatingresponsivewebappsthatarefarmorecapableandflexiblethanwouldbepossiblewithCSSalone.Thefollowingsectiongivesanexample.

DeferringImageLoadingTheproblemwithsimplyhidinganimgelementisthatthebrowserstillloadsit;itjustnevershowsittotheuser.Thisisaridiculoussituationbecauseitiscostingmeandtheuserbandwidthtodownloadaresourcethatwon’teverbeshownonadevicewithasmallscreen.Tofixthis,IhavedefinedanewdatabindingcalledifAttrintheutils.jsfile,asshowninListing7-7.Thisbindingaddsandremovesanattributebasedonevaluatingacondition.

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:theattrpropertyspecifieswhichattributeIwanttoapply,thetestpropertydetermineswhethertheattributeisaddedtotheelement,

Page 179: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER7CREATINGRESPONSIVEWEBAPPS

179

andthevalueattributespecifiesthevaluethatwillbeassignedtotheattributeiftestistrue.Listing7-8showshowIcanapplythisbindingtomyCheeseLuxlogomarkuptodeferloadingtheimageuntilitisrequired.

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.Totakeadvantageofthis,IusetheifAttrattributewiththelargeScreenviewmodelitemsothatthesrcattributeissetonlywhentheimagewillbedisplayed.Inthisway,Iamabletopreventtheimagefromloadingunlessitwillbeshown.Thisisaprettysimpletrickbutdemonstratesthekindofflexibilitythatyoushouldlookforwhencreatingaresponsivewebapp.

TipItisimportanttodistinguishbetweenresourcesthatyoudon’twanttouseimmediatelyfromresourcesthatyouareunlikelytowantatall.Ifyouhaveareasonableexpectationthattheuserwillrequireanimageinthenormaluseofyourapplication,thenyoushouldletthebrowserdownloaditsothatitisimmediatelyavailablewhenrequired.UsetheifAttrtechniquetoavoidawasteddownloadifitisunlikelythattheuserwillrequirearesource.

AdaptingtheWebAppLayoutFromthispointon,IsimplyhavetoadapteachpartofthewebapptothetwocategoriesofscreenthatIaminterestedin.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>

Page 180: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER7CREATINGRESPONSIVEWEBAPPS

180

<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>

Page 181: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER7CREATINGRESPONSIVEWEBAPPS

181

<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">

Page 182: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER7CREATINGRESPONSIVEWEBAPPS

182

<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>

Thejoyofthisapproachishowfewchangesarerequiredtomakeawebappresponsivetoscreensize(andhowsimplethosechangesare).Thatsaid,thereareasmallnumberofchangesthatrequireexplanation,whichIprovideinthefollowingsections.YoucanseehowmyresponsivewebappappearsonlargeandsmallscreensinFigure7-4.

Figure7-4.Thesamewebappdisplayedonalargeandsmallscreen

Page 183: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER7CREATINGRESPONSIVEWEBAPPS

183

Thesesmallchangeshaveabigimpact,andforthemostpart,thechangesarecosmetic.Theunderlyingfeaturesandstructureofmywebappremainthesame.Idon’thavetoforgomyviewmodelorroutingjusttosupportadevicewithasmallerscreen.

AdaptingtheSourceDataThecategorybuttonsareaproblemonasmallscreen,soIwanttodisplaysomethingtotheuserthatismeaningfulbutrequireslessscreenspace.Todothis,Imadesomeadditionstotheproducts.jsonfilesothateachcategorycontainsanametobeusedwhenspaceislimited.Listing7-10showstheadditionforoneofthecategories.

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.Icouldhavearrivedattheshortnamebysplittingthecategoryvaluestringonthespacecharacter,butIwanttomakethepointthatitisnotjustthescriptandmarkupinawebappthatcanberesponsive;youcanalsosupportthisconceptinthedatathatdrivesyourapplication.

InListing7-9,Imodifiedthedatabindingforthenavigationbuttonstotakeadvantageoftheshortercategoriesnames,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.Thisallowsmetousethesamesetofnavigationroutesirrespectiveofthescreensize(seeChapter4fordetailsofusingroutinginawebapp).

ApplyingConditionaljQueryUIStylingInthelargescreenlayout,Iresizetheproductlistelementstomakeroomforthebasket.Inthesmallscreenlayout,Ireplacethededicatedbasketwithaone-linetotalattheendofeachsection.IliketotakeadvantageofthematchMedia.addListenerfeatureifitisavailable,whichmeansImustbeabletotogglebetweenthesmallandlargescreenlayoutsasneeded.Toaccommodatethis,Itreatthosescript

Page 184: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER7CREATINGRESPONSIVEWEBAPPS

184

statementsthatdrivetheindividuallayoutsintheirownfunctionandregisterthatfunctionasasubscribertochangesintheviewmodel:

function performScreenSetup(smallScreen) { $('div.cheesegroup').not("#basket").css("width", smallScreen ? "" : "50%"); }; cheeseModel.device.smallScreen.subscribe(performScreenSetup);

Thefunctionwillbecalledonlywhenthevaluechanges,soIcallthefunctionexplicitlytogettherightbehaviorwhenthedocumentisfirstloaded,likethis:

performScreenSetup(cheeseModel.device.smallScreen());

Ineffect,ItoggletheCSSwidthpropertyofthedivelementsinthecheesegroupclassbasedonthesizeofthescreen.Youcouldignorethisapproachandjustleavethelayoutinitsinitialstate,butIthinkthatisalostopportunitytoprovideaniceexperiencefordesktopusers.

RemovingElementsfromtheDocumentForthemostpart,Isimplyhideandshowelementsinthedocumentbasedonthesizeofthescreen.However,thereareoccasionswhentheifandifnotbindingsarerequiredtoensurethatelementsarecompletelyremovedfromthedocument.AsimpleexampleofthiscanbeseeninthelistingwhereIusetheifbindingfortheone-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.cssfileisaCSSstylethatappliesroundedcorners:

div.groupcontent:last-child { border-bottom-left-radius: 8px; border-bottom-right-radius: 8px; }

Thebrowserdoesn’ttakeintoaccountthevisibilityofelementswhenworkingoutwhichisthelastchildofitsparent.IfIhadusedthevisiblebinding,thenIdon’tgettheroundedcornersIwantinthelargescreenlayout.TheifbindingforcesthebehaviorIwantbyremovingtheelementsentirely,ensuringthattheroundedcornersareappliedcorrectly.

RespondingtoScreenOrientationManymobiledevicesrespondtothewaythattheuserisholdingthedevicebychangingthescreenorientationbetweenlandscapeandportraitmodes.Keepinginformedofthedisplaymodeturnsouttobequitetricky,butitisworthdoingtomakesurethatyourwebapprespondsappropriatelywhentheorientationchanges.Thereareseveralwaystoapproachthisissue.

Somedevicessupportawindow.orientationpropertyandanorientationchangeeventtomakeiteasiertokeeptrackofthescreenorientation,butthisfeatureisn’tuniversal,andevenwhenitisimplemented,theeventtendstobefiredwhenitshouldn’tbe(andisn’tfiredwhenitshouldbe).

Page 185: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER7CREATINGRESPONSIVEWEBAPPS

185

Otherdevicessupportorientationaspartofamediaquery.ThisisusefuliftheaddListenerfeatureissupportedaspartofmatchMedia,butmostmobilebrowsersdon’tsupportthisfeature,andthesearethedeviceswhoseorientationismostlikelytochange.

Almostallbrowserssupportaresizeevent,whichistriggeredwhenthewindowisresizedortheorientationischanged.However,someimplementationsintroducedelaysbetweenorientationchangesandtheeventbeingtriggered,whichmakesforawebappthatisslowtorespondandthatmaychangeitslayoutorbehavioraftertheuserhasstartedinteractingintheneworientation.

Thefinalapproachistoperiodicallycheckscreendimensionsandworkouttheorientationmanually.Thisiscrudebuteffectiveandworksonlyifthefrequencyofthecheckishighenoughtomakeforarapidresponsebutlowenoughnottooverwhelmthedevice.

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); });

Page 186: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER7CREATINGRESPONSIVEWEBAPPS

186

} deviceConfig.largeScreen = ko.computed(function() { return !deviceConfig.smallScreen(); }); setInterval(function() { deviceConfig.smallScreen(window.innerWidth <= 500); }, 500); callback(deviceConfig); } }); };

Ihavesetuptwoviewmodeldataitems,landscapeandportrait,followingthesamepatternthatIusedforsmallScreenandlargeScreen.Idon’twanttoduplicatemycodefortestingtheorientationofthedevice,soIhavecreatedasimpleinlinefunctioncalledsetOrientationthatsetsthevalueofthelandscapedataitem:

var setOrientation = function() { deviceConfig.landscape(window.innerWidth > window.innerHeight); }

IhavefoundcomparingtheinnerWidthandinnerHeightvaluesofthewindowobjecttobethemostreliablewayoffiguringoutthescreenorientation.Thescreen.widthandscreen.heightvaluesshouldwork,butsomebrowsersdon’tchangethesevalueswhenthedeviceisreoriented.Thewindow.orientationpropertyprovidesgoodinformation,butitisn’tuniversallyimplemented.Thisisanundoubtedcompromise,andIrecommendyoutesttheefficacyofthisapproachonyourtargetdevices.

TherestoftheadditionsimplementthevariousmeansbywhichthesetOrientationwillbecalled:viatheorientationchangeandresizeevents,viaamediaquery,andviapolling.Judgingtherightfrequencytopolltheorientationisdifficult,butIusuallyuse500milliseconds.Itisn’talwaysasresponsiveasIwouldlike,butitstrikesareasonablebalance.

TipIcouldhaveusedasinglesetIntervalcalltopollforboththescreensizeandtheorientation,butIprefertokeeptheregionsofcodefunctionalityasseparateaspossible.

IntegratingScreenOrientationintotheWebAppIcanmakethewebapprespondtothescreenorientationnowthattheviewmodelhastheportraitandlandscapeitems.Todemonstratethis,Iamgoingtofixaproblem:thewebappcurrentlyrequirestheusertoscrolldowntoseealloftheelementsinlandscapemodeonadevicethathasasmallscreen.Figure7-5showstheproblemandtheresultafterIhavemodifiedthewebapplayout.

Page 187: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER7CREATINGRESPONSIVEWEBAPPS

187

Figure7-5.Respondingtothelandscapeorientationonsmallscreens

Torespondtothisorientationforsmallscreens,Ihaveremovedthecategorynavigationelementsandreplacedthemwithleftandrightbuttonsthatpagethroughthecategories.Thisisn’tthemostelegantapproach,butitmakesgooduseoflimitedscreenspacewhilepreservingthebasicnatureofthewebapp.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>

IremovetheelementsfromtheDOMifthedevicehasasmallscreenandisinthelandscapeorientation.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,butthecodethathandlesthenavigationthatariseswhenclickedisworthlookingat:

5

Page 188: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER7CREATINGRESPONSIVEWEBAPPS

188

... 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.Iwanttheusertobeabletorepeatedlyclickthesebuttons,andasImentionedalready,thebrowserwon’trespondtoanattempttonavigatetothesameURLthatisalreadybeingdisplayed.Withthisinmind,IhaveusedthejQueryclickmethodtohandletheregularJavaScripteventbycallingtheadvanceCategoryfunction.Idefinedthisfunctioninutils.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,soIenumeratethroughthedatatofindtheindexofthecurrentlyselectedcategoryandincrementordecrementthevaluebasedonwhichbuttonhasbeenclicked.Theresultisamorecompactlayoutthatbettersuitsthesmall-screenlandscapeorientation.ThewayIhavecategorizeddevicesisprettycrude,andIrecommendyoutakeamoregranularapproachinrealprojects,butitservestodemonstratethetechniquesyouneedinordertorespondtoscreenorientation.

RespondingtoTouchThefinalfeaturethataresponsivewebappneedstodealwithistouchsupport.Theideaoftouch-basedinteractionisfirmlyestablishedinthesmartphoneandtabletmarkets,butitisalsomakingitswaytothedesktop,mostlythroughMicrosoftWindows8.

Page 189: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER7CREATINGRESPONSIVEWEBAPPS

189

Tosupporttouchinteraction,weneedtwothings:atouchscreenandabrowserthatemitstouchevents.Thesetwodon’talwayscometogether;pluggingatouch-enabledmonitorintoadesktopmachinedoesn’tautomaticallyenabletouchinthebrowser,forexample.Equally,youshouldnotassumethatifadevicesupportstouchthatthiswillbetheonlymodelforinteractions.Manydeviceswillsupportmouseandkeyboardinteractionsalongsidetouch,andtheusershouldbeabletopickwhichevermodelsuitsthemwhenusingyourwebappandswitchfreelybetweenthem.

Devicesthatdon’thavearegularmouseandkeyboardsynthesizeeventssuchasclickinresponsetotouchevents.Thismeansyoudon’tneedtomakechangestoyourwebapptosupportbasictouchinteractions.However,tocreateatrulyresponsewebapp,youshouldconsidersupportingthenavigationgesturesthatarecommonontouchdevices,suchasswiping.Idemonstratehowtodothisshortly.

DetectingTouchSupportThereisaW3Cspecificationfortouchevents,butitislow-level,andalotofworkisrequiredtofigureoutwhatgesturestheuserismaking.AsIhavesaidbefore,partofthejoyofwebappdevelopmentistheavailabilityofhigh-qualityJavaScriptlibrariesthatmakedevelopmentsimpler.OnesuchexampleistouchSwipe,whichbuildsonjQueryandtransformsthelow-leveltoucheventsintoeventsthatrepresentgestures.IincludedthetouchSwipelibraryinthesourcecodedownloadthataccompaniesthisbookandthatisavailablefromApress.com.Thewebsiteforthelibraryishttp://labs.skinkers.com/touchSwipe.

ThesimplestandmostreliableapproachtodetectingtouchsupportistorelyontheModernizrtest.Listing7-14showstheadditionstothedetectDeviceFeaturesfunctionintheutils.jsfiletodetectandreportontouchsupportandshowstheuseoftouchSwipetorespondtotouchevents.

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); } }

Page 190: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER7CREATINGRESPONSIVEWEBAPPS

190

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.IhaveaddedatestthatusestheModernizr.touchcheckandthatloadsthetouchSwipelibraryiftouchsupportispresent.

TipMakesureyouincludedthetouchtestsifyoudownloadedyourownversionofModernizr.TheversionIincludedinthesourcecodeforthischaptercontainsalloftheavailabletests.

NoticethatIusedthecallbackpropertytosetupsupportforhandlingswipes.Functionssetusingthecallbackpropertyareexecutedwhenthespecifiedresourcesareloaded,whereasfunctionsspecifiedusingcomplete areexecutedattheendofthetest,irrespectiveofthetestresult.IwanttohandleswipeeventsonlyiftouchSwipehasbeenloaded(whichitselfindicatesthattouchsupportispresent),soIhaveusedcallbacktogiveModernizrmyfunction.

ThetouchSwipelibraryisappliedusingtheswipemethod.Inthisexample,Ihaveselectedthehtmlelementasthetargetfordetectingswipegestures.Somebrowserslimitthebodyelementsizesothatitdoesn’tfilltheentirewindowwhenthecontentissmallerthantheavailablespace.Thisisn’tusuallya

Page 191: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER7CREATINGRESPONSIVEWEBAPPS

191

problem,butitcreatesdeadspotsonthescreenwhendealingwithgestures,whichmaynotbetargetedatindividualelements.Thesimplestwaytogetaroundthisistoworkonthehtmlelement.

ThetouchSwipelibraryisabletodifferentiatebetweendifferentkindsoftoucheventsandswipesinarangeofdirections.Icareaboutswipesonlytotheleftandtherightinthisexample,whichiswhyIhavedefinedafunctionfortheswipeLeftandswipeRightpropertiesintheobjectIpassedtotheswipemethod.InbothcasesIhavespecifiedtheadvanceCategoryfunction,whichisthesamefunctionIusedtochangeselectedcategoriesearlier.Theresultisthatswipingleftmovestothepreviouscategoryandswipingrightgoestothenextcategory.ThelastpointtonoteaboutthislistingisthelastiteminthearraypassedtotheModernizr.loadmethod:

{ complete: function() { callback(deviceConfig); } }

Idon’twanttoinvokethecallbackfunctionuntilIhavesetupallofthedevicedetailsintheresultobjectthatwillbeaddedtotheviewmodel.Theeasiestwaytoensurethishappensistocreateanadditionaltestthatcontainsjustacompletefunction.Modernizrwon’texecutethisfunctionuntilalloftheothertestshavebeenperformed,therequiredresourceshavebeenloaded,andthecallbackandcompletefunctionsforalloftheprevioustestshavebeenperformed.

UsingTouchtoNavigatetheWebAppHistoryInthepreviousexample,Irespondtoswipegesturesbyloopingthroughtheavailableproductcategories.Inthissection,Ishowyouhowtorespondtothesegesturesinamoreusefulway.

Thetemptationistousethebrowser’shistorytorespondtoswipes.Theproblemisthatthereisnowaytopeekatthepreviousornextentryinthehistoryandseewhetheritisonethatbelongstothewebapp.Ifitisn’t,thenyouendupmakingtheusernavigateawayfromyourwebapp,potentiallytoaURLthattheyhadnointentionofvisiting.Listing7-15showsthechangesrequiredtotheenhanceViewModelfunctionintheutils.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");

Page 192: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER7CREATINGRESPONSIVEWEBAPPS

192

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.IhaveaddedanindexandanarraytotheviewmodelandsubscribedtotheselectedCategoryobservabledataitemsothatIcanbuilduptheuser’shistoryastheychangecategories.IhavenotworriedaboutmanagingthesizeofthearraysinceIthinkitisunlikelythatenoughcategorychangeswillbemadetocauseacapacityproblem.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]); } } }

Page 193: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER7CREATINGRESPONSIVEWEBAPPS

193

Ihavetobecarefulnottoapplytheswipehistorywhenthewebappisdisplayedonasmallscreeninthelandscapeorientation.Iremovedthecategorybuttonsinthisdeviceconfiguration,meaningthatthereisnowayfortheusertogenerateahistoryformetonavigatethrough.Inallotherdeviceconfigurations,Iamabletorespondtotheswipebychangingthevalueoftheindexandselectingthecorrespondinghistoriccategory.Theresultisthattheusercannavigatebetweencategoriesusingthenavigationbuttonsandswipingmovesbackwardorforwardthroughtherecentselections.

IntegratingwiththeApplicationRoutesThelasttweakIwanttomakeistorespondtotheswipeeventsthroughthewebapp’sURLroutes.Inthelastlisting,Itooktheshortcutofchangingtheobservabledataitemdirectly,butthismeansIwillbypassanycodethatisgeneratedasaresultofaURLchange,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.SinceIhavespecifiedrelativeURLs,thebrowserwillnotnavigateawayfromthewebapp,andmyrouteswillbeabletomatchtheURLs.Bydoingthis,Iensurethatmyresponsetoswipeeventsisconsistentwithotherformsofnavigation.

Page 194: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER7CREATINGRESPONSIVEWEBAPPS

194

SummaryInthischapter,Ihaveshownyouthethreecharacteristicsthatyoumustadapttoinordertocreatearesponsivewebapp:screensize,screenorientation,andtouchinteraction.Bydetectingandadaptingtodifferentdeviceconfigurations,youcancreateonewebappthatcanseamlesslyandelegantlyadaptitslayoutandinteractionmodeltosuittheuser’sdevice.Theadvantagesofsuchanapproachareobviouswhenyouconsidertheproliferationofsmartphonesandtabletsandtheblurringofthedistinctionsbetweenthesedevicesanddesktops.Inthenextchapter,Ishowyouadifferentapproachtosupportingdifferenttypesofdevices:creatingaplatform-specificwebapp.

Page 195: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

C H A P T E R 8

195

Creating Mobile Web Apps

Analternativetocreatingawebappthatadaptstothecapabilitiesofdifferentdevicesistocreateaversionthatisspecificallytargetedtomobiledevices.Choosingbetweenaresponsivewebappandamobile-specificimplementationcanbedifficult,butmyruleofthumbisthatamobileversionmakessensewhenIwanttoofferaradicallydifferentexperiencetomobileanddesktopusersorwhendealingwithdeviceconstraintsinaresponsiveimplementationbecomesunwieldyandoverlycomplex.Yourdecisionwill,ofcourse,dependonthespecificsofyourproject,butthischapterisforwhenyoudecidethatoneversionofyourwebapp,howeverresponsive,won’tcatertoyourmobileusers’needs.

DetectingMobileDevicesThefirststepistodecidehowyouaregoingtodirectusersofmobiledevicestothemobileversionofyourwebapp.Thedecisionyoumakeatthisstagewillshapealotoftheassumptionsyouwillhavewhenyoucometobuildthemobilewebapp.Thereareacoupleofbroadapproaches,whichIdescribeinthefollowingsections.

DetectingtheUserAgentThetraditionalapproachistolookattheuseragentstringthatthebrowserusestodescribeitself.Thisisavailablethroughthenavigator.userAgentproperty,andthevaluethatitreturnscanbeusedtoidentifythebrowserand,usually,theplatformthebrowserisrunningon.Asanexample,hereisthevalueofnavigator.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"

Youcanidentifymobiledevicesbybuildingalistofuseragentvaluesandkeepingtrackofwhichonesrepresentmobilebrowsers.Youdon’thavetocreateandmanagetheselistsyourself,however—therearesomegoodsourcesofinformationavailableonline.(AverycomprehensivedatabasecalledWURFLcanbefoundathttp://wurfl.sourceforge.net,butthisrequiresintegrationintoyourserver-sidecode,whichisnotidealforthisbook.)

Aless-comprehensiveclient-sidesolutioncanbefoundathttp://detectmobilebrowsers.com,whereyoucandownloadasmalljQuerylibrarythatmatchestheuseragentagainstaknownlistofmobilebrowsers.Thisapproachisn’tascompleteasWURFL,butitissimplertouse,anditdetectsthemostwidelyusedmobilebrowsers.Todemonstratethiskindofmobiledevicedetection,IdownloadedthejQuerycodetomyNode.jscontentdirectoryinafilecalleddetectmobilebrowser.js(youcanfindthis

Page 196: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

196

fileinthesourcecodedownloadforthisbook,availablefromApress.com).Listing8-1showshowtousethisplugintodetectmobiledevices.

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,Icanchecktoseewhethermywebappisrunningonamobilebrowserbyreadingthe$.browser.mobileproperty,whichreturnstrueiftheuseragentisrecognizedasbelongingtoamobilebrowser.Inthiscase,Iredirectmobileuserstothemobile.htmldocument,whichIwillusetobuildmymobilewebapplaterinthischapter.

Themainproblemwithusingtheuseragentisthatitisn’talwaysaccurate,andasImentionedinthepreviouschapter,thedistinctionsbetweenmobileanddesktopdevicesarebecomingblurred.Inessence,yourelyonsomeoneelse’sdecisionaboutwhatdefinesmobile,andthatwon’talwayslineupwiththewayyouwanttosegmentyouruserbase.And,althoughthelistsofbrowseraregenerallyaccurate,itcantakeawhilefornewmodelstobeproperlyidentifiedandcategorized,especiallyfromnichehardwareproviders.

Arelatedproblemisthatmanybrowsersallowtheusertochangetheuseragentsothatanotherbrowserisidentified.Notmanyusersmakethischange,butitdoesmeanyoucannotentirelyrelyontheuseragentreportedthroughthenavigator.userAgentproperty.

DetectingDeviceCapabilitiesIprefertoclassifyadeviceasmobilebydetectingitscapabilities,muchasIdidinChapter7.Thisallowsmetodecidewhatdefinesmobileinthecontextofthewaymywebappworks.FortheCheeseLuxwebapp,Ihavedecidedthatdevicesthataretouchenabledandthathavescreensthatarenarrowerthan500pixelswillbegiventhemobileversionofmywebapp.YoucanseehowIhaveimplementedthispolicyinListing8-2,whichshowsthechangestothedetectDeviceFeaturesfunctionfromtheutils.jsfile.

Page 197: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

197

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;itreturnstrueifthedevicemeetsmycriteriaforgettingthemobileversionofmywebapp.Listing8-3showshowIhaveusedthisnewpropertyinexample.html.

Listing8-3.UsingMobileDeviceDetectionintheMainWebAppDocument

var cheeseModel = {}; detectDeviceFeatures(function(deviceConfig) { cheeseModel.device = deviceConfig; if (cheeseModel.device.mobile) { location.href = "mobile.html"; }

Page 198: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

198

$.getJSON("products.json", function(data) { cheeseModel.products = data; }).success(function() { $(document).ready(function() { ...

IaddthecapabilitiescheckbeforetheJSONdataisloadedsothatIcandirecttheusertomobile.htmlbeforeIstartmakingnetworkrequestsandprocessingtheelementsintheDOM.

TipInthisexampleandthepreviousone,IplacedthemobiledetectioncodeoutsideofthejQueryreadyeventsothatthebrowserwillexecutethecodeassoonasitreachesitinthedocument.AmorethoroughapproachwouldbetoplacethedetectioncoderightatthetopofthedocumentsothatitisexecutedbeforeanyoftheJavaScriptlibrariesareloaded.However,sinceIrelyonsomeoftheselibrariestoactuallyperformthedetection,carefulorderingofthescriptelementsisrequired.

CreatingaSimpleMobileWebAppBothoftheapproachesIshowedyouassumethattheuserwillwanttoviewthemobileversionofmywebapp—butthiswon’talwaysbethecase.Iprefertoidentifyamobiledeviceandthenasktheuserwhattheywanttodo.Thisapproachputscontrolintousers’hands(whichiswhereitshouldbe),butitdoesmeanthatIhavetoprovideamechanismforlettingthemchooseandrememberingthechoicetheymake.So,ratherthansimplydirectingmobiledevicestothemobileversionofthewebapp,Iuseaninterimdocumentcalledaskmobile.html.IplacedthisfileintheNode.jscontentdirectory,andyoucanseethefilecontentinListing8-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=/"; }

Page 199: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

199

$(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.

Thisdocumentpresentstheuserwithtwobuttonsthattheycanusetochoosetheversionofthewebapptheywanttouse.YoucanseehowthedocumentisdisplayedinthebrowserinFigure8-1.

Page 200: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

200

Figure8-1.Askingtheuserwhichversionofthewebapptheyrequire

ThistinywebappgivesmeagoodexamplewithwhichtointroducejQueryMobile,whichiswhatI’llbeusinginthischapter.jQueryMobileisatoolkitoptimizedformobiledevices,anditincludeswidgetsthatareeasytointeractwithusingtouchandbuilt-insupportforhandlingtoucheventsandgestures.

jQueryMobileisthe“official”mobiletoolkitfromthemainjQueryproject,andit’sprettygood,althoughtherearesomeroughedgeswithsomelayoutsthatneedtweakingwithminorCSS.ThereareotherjQuery-basedmobilewidgettoolkitsavailable—andsomeofthemareverygoodaswell.IhavechosenjQueryMobilebecauseitsharesabroadlycommonapproachwithjQueryUIandithassomedesigncharacteristicsthataretypicalofmostmobiletoolkitsandthatrequirespecialattentionwhenwritingcomplexwebapps.

AVOIDING PSEUDONATIVE MOBILE APPS

AnotherreasonthatIusejQueryMobileisthatitdoesn’ttrytore-createtheappearanceofanativesmartphoneapplication,whichisanapproachthatsomeoftheothertoolkitsadopt.Idon’tlikethatapproachbecauseitdoesn’tquitework.IfyougivetheusersomethingthatlookslikeanativeiOSorAndroidapp,thenyouneedtomakesureitbehavesexactlythewayanativeapplicationshould—and,atleastatthemoment,thatisn’tpossible.

Theworstpossibleapproachistotrytore-createanativeappforjustoneplatform.Youoftenseethis,anditisusuallyiOSthatwebappdevelopersaimfor.Thismightnotbesobadifthere-creationwasfaithfulandallmobiledevicesraniOS,butusersofAndroidandotheroperatingsystemsgetsomethingthatistotallyalien,andiOSusersgetsomethingthatinitiallyappearstobefamiliarbutthatturnsouttobeconfusingandinconsistent.

Tomymind,itisfarbettertodesignawebappthatisgenuinelyobviousandeasytouse.Theresultsarebetter,youuserswillbehappier,andyoudon’thavetocontortyourwebapptofitinsidetheconstraintsofplatformthatyoucan’tproperlyadheretoanyway.

Page 201: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

201

IamnotgoingtoprovidealengthytutorialonjQueryMobile,buttherearesomeimportantcharacteristicsthatIneedtoexplaininordertodemonstratehowtocreateasolidmobilewebapplication.Iexplainthecoreconceptsinthesectionsthatfollow.IfyouwantmoreinformationaboutjQueryMobile,thenseetheprojectwebsiteormyProjQuerybook,whichispublishedbyApressandcontainsacompletereferenceforusingjQueryMobile.

InstallingjQueryMobileYoucandownloadjQueryMobilefromhttp://jquerymobile.com.jQueryMobiledependsonjQuery,andthescriptelementthatimportsjQueryintothedocumentmustcomebeforetheonethatimportsthejQueryMobilelibrary,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.jscontentdirectoryalongwiththeJavaScriptfile,andputtheimagesintotheimagesdirectoryalongwiththosefromjQueryUI.

UnderstandingthejQueryMobileDataAttributesjQueryMobilereliesondataattributestoconfigurethelayoutofthewebapp.Dataattributesallowcustomattributestobeappliedtoelements,justlikethedata-bindattributethatIhavebeenusingfordatabindings.Thereisnodata-bindattributedefinedintheHTMLspecification,butanyattributethatisprefixedbydata-isignoredbythebrowserandallowsyoutoembedusefulinformationinyourmarkupthatyoucanthenaccessviaJavaScript.DataattributeshavebeenusedunofficiallyforafewyearsandareanofficialpartofHTML5.

jQueryMobileusesdataattributesratherthanthecode-centricapproachthatjQueryUIrequires.Youusethedata-roleattributetotelljQueryMobilehowitshouldtreatanelement—themarkupisprocessedautomaticallywhenthedocumentisloadedandthewidgetsarecreated.

Youdon’talwaysneedtousethedata-roleattribute.Forsomeelements,jQueryMobilewillassumethatitneedstocreateawidgetbasedontheelementtype.Thishashappenedforthebuttonsinthedocument: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>

DefiningPagesThemostimportantvalueforthedata-roleattributeispage.Whenbuildingmobilewebapps,itisgoodpracticetominimizethenumberofrequestsmadetotheserver.jQueryMobilehelpsinthisregardbysupportingsingle-pageapps,wherethemarkupandscriptformultiplelogicalpagesiscontainedwithinasingledocumentandshowntotheuserasrequired.Apageisdenotedbyadivelementwhosedata-roleattributeispage.Thecontentofthedivelementisthecontentofthatpage:

Page 202: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

202

... <body> <div id="page1" data-role="page" data-theme="a"> ...page content goes here... </div> </body> ...

Thereisjustonepageinmyaskmobile.htmldocument,butI’llreturntothetopicofpageswhenwebuildthefullmobileCheeseLuxapplaterinthechapter.

ConfiguringWidgetsjQueryMobilealsousesdataattributestoconfigurewidgets.Bydefault,jQueryMobilebuttonsspantheentirepage.Thisgivesalargetargettohitonasmallportraitscreenbutlooksprettyoddinotherlayouts.Todisablethisbehavior,IhavetoldjQueryMobilethatIwantinlinebuttons,wherethebuttonisjustlargeenoughtocontainitscontent.Ididthisbysettingthedata-inlineattributetotrue forthebuttonelements,likethis:

<button data-inline="true" id="no">No</button>

Anumberofelement-specificdataattributesareavailable,andyoushouldconsultthejQueryMobilewebsitefordetails.OneimportantconfigurationattributethatIwillmention,however,isdata-theme,whichappliesastyletothepageorwidgettowhichitisapplied.AjQueryMobilethemecontainsanumberofswatches,namedA,B,C,andsoon.Ihavesetthedata-themeattributetoaforthepageelementsoastosetthethemeforthesinglepageinthedocumentandallofitscontent:

<div id="page1" data-role="page" data-theme="a">

YoucancreateyourowncustomthemesusingthejQueryMobileThemeRoller,whichisavailableatjquerymobile.com.Iamusingthedefaultthemes,andswatchAprovidesthedarkstyleforthewebapp.Forcontrast,IhavesettheswatchontheYesbuttontob,likethis:

<button data-inline="true" data-theme="b" id="yes">Yes</button>

ButtonsinswatchBareblue,whichgivestheuserastrongsuggestionastotherecommendeddecision.

TipIhavedefinedanewCSSstylesheetforusewithjQueryMobile.Itiscalledhttp://styles.mobile.css,anditlivesintheNode.jscontentdirectoryalongwiththeotherexamplefiles.Thestylesinthisfilejusttweakthelayoutslightly,allowingmetocenterelementsinthepageandmakeotherminoradjustmentstothedefaultjQueryMobilelayout.Youcanfindthestylesheetinthesourcecodedownloadforthisbook,whichisavailablefromApress.com.

Page 203: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

203

DealingwithjQueryMobileEventsUsingawidgetlibrarythatisbasedonjQuerymeanswecanhandleeventsusingfamiliartechniques.Ifyoulookatthescriptelementintheaskmobile.htmldocument,youwillseethathandlingtheeventstriggeredwhenthebuttonsareclickedrequiresthesamebasicjQuerycodethatIhavebeenusingthroughoutthisbook:

<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.Hereitis:

$(document).bind("pageinit", function() { ...code to handle button click events... }

jQueryMobileprocessesthemarkupfordataattributeswhenthestandardjQueryreadyeventfires.ThismeansIhavetobindtothepageiniteventifIwanttoexecutecodeafterjQueryMobilehasfinishedsettingupitswidgets.ThereisnoconvenientmethodforspecifyingafunctionforthiseventandsoIhaveusedthebindmethodinstead.ThecodeinthisexamplewouldhaveruninresponsetothejQueryreadyeventquitehappily,sinceIamnotinteractingdirectlywiththewidgetsthatjQueryMobilecreates.ThiswillchangewhenIcometothefulljQueryMobileCheeseLuxwebapp,anditisgoodpracticetousethepageiniteventinalljQueryMobileapps.

StoringtheUser’sDecisionNowthatIhavedescribedthejQueryMobilepartsofaskmobile.html,wecanreturntotheapplication’sfunction,whichistorecordandstoretheuser’spreferencefortheversionofthewebapptheuserwantstouse.Iuselocalstorageifitisavailableandfallbacktoaregularcookieifitisnot.ThereisnoconvenientjQuerysupportforworkingwithcookies,soIhavewrittenmyownfunctioncalledsetCookie:

Page 204: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

204

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,afterwhichthebrowserwilldeletethecookieandtheuserwillhavetoexpresstheirpreferenceagain.Forbrevity,Ihavenotsetanylifetimewhenusinglocalstorage,butdoingsowouldbegoodpractice.

TipItisalsogoodpracticetoasktheuseriftheywantyoutostoretheirchoiceatall.Ihaven’ttakenthisstepinmysimpleexample,butsomeusersaresensitivetotheseissues,especiallywhenitcomestocookies.

DetectingtheUser’sDecisionintheWebAppThelaststepistodetecttheuser’sdecisioninthedesktopversionoftheCheeseLuxwebapp.Listing8-5showsapairoffunctionsIhaveaddedtoutils.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; }

Page 205: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

205

ThecheckForVersionPreferencefunctionusestheviewmodelvaluestoseewhethertheuserhasamobiledeviceand,ifso,triestorecovertheresultofapreviousdecisionfromlocalstorageoracookie.Cookiesareawkwardtoprocess,soIhaveaddedagetCookiefunctionthatfindsacookiebynameandreturnsitsvalue.Ifthereisnostoredvalue,thenIdirecttheusertotheaskmobile.htmldocumenttogettheirpreference.Ifthereisastoredvalue,thenIuseittoswitchtothemobileversionifthatwastheuser’spreference.AllthatremainsistoincorporateacalltothecheckForVersionPreferencefunctionintoexample.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’twanttousepagesinachapteronmobiledevicestolistthedesktopwebappcode.YoucangetthecompletelistingaspartofthesourcecodedownloadavailablefreeofchargefromApress.com.

TipItmakessensetooffertheuserthechancetochangetheirmindswhentheeffectofthedecisionisstoredandappliedautomatically.IskippedthisstepbecauseIwanttofocusonthemobileappinthischapter,butyoushouldalwaysincludesomekindofUIcuethatallowstheusertoswitchtotheotherversionofthewebapp,especiallyifthedecisionisstoredandusedpersistently.

BuildingtheMobileWebAppIamgoingtostartwithabasicmobileversionoftheCheeseLuxwebappandthenbuildonittoshowyouhowtocreateabetterexperiencefortheuser.WhenIcreateamobileversionofawebappthathasadesktopcounterpart,Ihavetwogoalsinmind:

• Reuseasmuchdesktopcodeasispossible

• Ensurethatthemobilerespondselegantlytodifferentdevicecapabilities

Thefirstgoalisallaboutlong-termmaintainability.ThemorecommoncodeIhave,thefeweroccasionstherewillbewhereIhavetofindandfixabugintwodifferentplaces.Iliketodecideinadvancewhichversionofthewebapphasprimacyandwhichwillhavetoflextobeabletousethecode.

Page 206: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

206

Ingeneral,Itendtocreatethedesktopversionfirstandmakethemobilewebappadapt.Theexceptiontothisiswhenthemajorityofuserswillbeusingmobiledevices.

WHAT ABOUT MOBILE FIRST?

Thereisaview(oftenreferredtoasmobilefirst)thatfocusesonthedesignanddevelopmentofthemobileplatformfirst,largelybecauseitforcesyoutoworkwithinthemostconstrainedenvironmentyouwillbetargetingandbecausemobiledeviceshavecapabilities,likegeolocation,thatarenotondesktops.

Inmyprojects,Idon’twantinitialconstraints—Iwanttobuildtherichest,deepest,andmostimmersiveexperienceIcan,and,forthemomentatleast,thatisthedesktop.OnceIhaveahandleonwhatispossiblewithlargescreensandrichinteraction,Ibegintheprocessofdealingwithdeviceconstraints,paringdownandtailoringmyappuntilIgetsomethingthatworkswellonamobiledevice.Iamnotabelieverintheuniquecapabilitiesofmobiledevices,either.AsImentionedinChapter7,thehardandfastdistinctionsbetweencategoriesofdevicesarefadingfast.OneofmymomentsofwonderrecentlywaswhenGooglewasabletousetheWi-FidataitcollectsalongwithitsStreetViewproducttopinpointmylocationwithinafewfeet.Thiswasonamachinethatwouldrequireaforklifttrucktobemobile.

But,asImentionedpreviously,Iamnotapatternzealot,andyoushouldfollowwhateverapproachmakesthemostsenseforyouandyourprojects.Don’tletanyonedictateyourdevelopmentstyle,includingme.

Thesecondgoalisaboutensuringthatmymobilewebappisresponsiveandadaptstothewide

rangeofdevicetypesthatusersmayhave.Youcannotaffordtomakeassumptionsaboutscreensizeandinputmechanismsevenwhentargetingjustmobiledevices.

CautionYoumaybetemptedtotrytocreateawebappthatswitchesbetweenjQueryUIandjQueryMobile(orequivalentlibraries)basedonthekindofdevicethatisbeingused.Suchatrickispossiblebutincrediblyhardtopulloffwithoutcreatingalotofverycontortedcodeandmarkup.Themostsensibleapproachistocreateseparateversionsifyouwanttotakeadvantageoffeaturesthatarespecifictoonelibraryoranother.

Togetthingsgoing,Listing8-6showsafirstpassatcreatingthecorefunctionalityusingjQueryMobile.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;

Page 207: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

207

}); </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); });

Page 208: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

208

}); </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">&nbsp;</button>

Page 209: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

209

<input type="submit" value="Submit Order"/> <button id="right" data-icon="arrow-r" data-iconpos="right">&nbsp;</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,thisisastraightforwardwebappthatreliesonthecorefunctionalityofjQueryMobile,butyouneedtobeawareofsomewrinklesandadditionsthatIdescribeinthefollowingsections.Youcanseethelandscapeandportraitlayoutsforasmall-screendeviceinFigure8-2.Thewebappalsosupportslayoutsformobiledeviceswithlargerscreens.Ihavenotshowntheselayouts,buttheyaresimilartothoseshowninthefigure,butwiththeCheeseLuxlogoandthefullcategorynamesdisplayedinthenavigationbuttons.

Figure8-2.ThebasicimplementationofthemobileCheeseLuxwebapp

Youwillnoticenewdatabindingsandviewmodelitemsinthislisting.TheformatTextdatabindingletsmeapplyaprefixandsuffixtothetextcontentofanelement,whichsimplifiesworkingwith

Page 210: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

210

composedstrings,especiallycurrencyamounts.ThisisoneofthesetofcustombindingsthatIgenerallyaddtoprojectsandthecode,whichisincludedintheutils.jsfile,asshowninListing8-7.ThecomposeStringfunctionusedbythisbindingisthesameoneIshowedyouinChapter4whenIintroducedthecustomformatAttrbinding.

Listing8-7.TheformatTextCustomDataBinding

ko.bindingHandlers.formatText = { update: function(element, accessor) { $(element).text(composeString(accessor())); } }

Theotheradditionsaresomehelpfulshortcutsaddedtothedevicecapabilitiesinformationintheviewmodel.AlthoughKOcandealwithexpressionsindatabindings,Idon’tlikedefiningcodeinthisway,andIgenerallycreatecomputeddataitemsthatallowmetodeterminethestateofthedevicethroughasingleviewmodelitem.Forthischapter,IdefinedapairofcomputedvaluesthatletmeeasilyreadthecombinationsofscreensizeandorientationthatIaminterestedinforthemobilewebapp.TheseshortcutsaredefinedinthedetectDeviceFeaturesfunctionintheutils.jsfile,asshowninListing8-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,

Page 211: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

211

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); } }]); }; ...

ManagingtheEventSequenceAsIdemonstratedintheaskmobile.htmldocument,jQueryMobilewillprocessadocumentautomaticallyandcreatewidgetsbasedonelementtypesandthevalueofthedata-roleattribute.Thisisanicefeature,anditsignificantlyreducestheamountofcoderequiredforsimplewebapps.Unfortunately,itgetsinthewaywhenyouareusingtheviewmodeltogenerateorformatelements,especiallyifthedataintheviewmodelisobtainedviaAjax.jQueryMobilewillprocessthedocument

w

Page 212: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

212

beforetheviewmodelispopulatedwiththedatabindings,whichmeansthatwidgetsarenotcreatedproperly.

ThisisthesameproblemIencounteredpreviouslywithjQueryUI,buttheissueisworsewithjQueryMobilebecauseitassumesthatithassolecontrolofelementsinapageandmakesitverydifficulttocreatebindingsthatcannegotiatetheextraelementsthatjQueryMobileuseswhenitsetsupawidget.(ThisisaproblemI’llreturntofordifferentreasonslaterinthischapter.)

DisablingAutomaticProcessingThebestapproachistopreventjQueryMobilefromautomaticallyprocessingthedocument.Todothis,Ineedtohandlethemobileinitevent,whichisemittedbyjQueryMobilewhenthelibraryisfirstloaded.IneedtoregistermyhandlerfunctionbeforejQueryMobileisloaded,whichmeansIhavetoinsertanewscriptelementaftertheonethatimportsjQueryandbeforetheonethatimportsjQueryMobile,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,IdisablethejQueryMobilefeaturethatprocessesthemarkupinthedocumentautomatically.

TipTobefair,IneedtoinsertmyscriptelementafterjQueryonlyifIwanttousethebindmethod,butIprefertodothisratherthanusetheclunkyDOMAPIforhandlingevents.

DisablingtheautomaticprocessingstopstheracebetweentheviewmodelandjQueryMobileandallowsmetomakemyAjaxrequest,populatetheviewmodel,anddoanyothertasksIneedwithoutworryingaboutprematurewidgetcreation.WhenIamdonesettingup,IexplicitlytelljQueryMobilethatitshouldprocessthepage,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();

Page 213: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

213

}); });

ThemobileobjectprovidesaccesstothejQueryMobileAPI,andtheinitializePagemethodstartspageprocessing.

RespondingtothepageinitEventNowthatIhavethemaineventsundercontrol,IcanusethepageinittoperformtasksafterjQueryMobilehasprocessedthepagesinthedocument.jQueryMobileisgenerallyverysolid,butithassomelayoutquirks.Oneinparticularisthatgroupsofbuttonsarenotcenteredinthepage.Forthebuttonsatthebottomofthepage,IhavebeenabletofixthisissuewithCSS(whichiswhatthecenteredstyleisforinthestyles.mobile.cssfile).Butthesizeofthenavigationbuttonschanges,andthatrequiresaJavaScriptsolution,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,whichisanidealuseforthepageinitevent.Inthefunction,Iaddupthewidthofthechildrenofeachfieldsetelementandthenusethetotalvaluetosetthewidthofthefieldset.jQueryMobileleavesthefieldsettobethewidthofthewindow,andthesequenceofelementsrequiredtocreateasetofbuttonsmakesithardtocenterthebuttonsbyothermeans.

TipIusethejQueryeachmethodsothatIcanbesurethatthechildrenmethodreturnsonlythechildrenofonefieldsetelement.Thismeansmycodewon’tbreakifIaddanotherfieldsetelementlater.Elementselectorsaregreedy,andifIjustcall$('fieldset').children(),Iwillgetthechildrenofallfieldsetelementsinthedocument,whichwillthrowoutthewidthcalculations.

Page 214: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

214

IwrappedthecodethatsetsthewidthinsideacalltothesetTimeoutfunctionbecauseIwanttocorrectlyresizethefieldsetelementwhenthecontentofthenavigationbuttonschange,whichhappenswhenthesizeandorientationarealtered.

Thecontentoftheelementsischangedbydatabindings,whichareexecutedwhenobservabledataitemsintheviewmodelareupdated.SinceIamusingthesubscribemethodtoreceivethesamekindofnotifications,Ineedtomakesurethatmycodetoresizethefieldsetisn’texecutedbeforethebuttoncontentischanged,whichIachievebyintroducingasmalldelayusingthesetTimeoutfunction.

PreparingforContentChangesjQueryMobileassumesthatithascontroloftheelementsthatareusedasthefoundationforwidgets.Inthecaseofbuttons,jQueryMobilewrapsthebuttoncontents(orlabelcontentswhenusingradiobuttons)inaspanelementsothatstylingcanbeapplied.

ThisisthesameproblemthatjQueryUIcreates,andthesolutionisthesameforjQueryMobile:wrapthecontentinaspanelementyourselfsothatyouhaveatargetfordatabindings.Onceyouhaveanelementthatyoucanattachdatabindingsto,youdon’tneedtoworryabouthowjQueryMobiletransformstheelementintoawidget.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,butalotofmobilewebappprogrammersgetcaughtbythisissueandenduptryingtoresolveitthroughsometorturedandunreliablealternative.Thissimpleapproachresolvestheproblemratherneatly.AllofthemobilewidgettoolkitsthatIhaveusedclashwithdatabindingsinasimilarway.InthecaseofjQueryMobile,youknowthattheproblemhasoccurredwhentheformattingofbuttonsislostwhenadatabindingchangesthebuttoncontent,asshowninFigure8-3.

Figure8-3.ProblemscausedbyjQueryMobileaddingelementsforstyling

Page 215: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

215

DuplicatingElementsandUsingTemplatesNotallconflictsbetweenwidgetlibrariesanddatabindingscanberesolvedsoeasily.InListing8-6,Icreatedduplicatesetsofthebuttonsthataredisplayedatthebottomofthepage,likethis:

<div class="middle" data-role="controlgroup" data-type="horizontal" data-bind="visible: device.smallAndLandscape()"> <button id="left" data-icon="arrow-l">&nbsp;</button> <input type="submit" value="Submit Order"/> <button id="right" data-icon="arrow-r" data-iconpos="right">&nbsp;</button> </div> <div class="middle" data-role="controlgroup" data-type="horizontal" data-bind="visible: !device.smallAndLandscape()"> <input type="submit" value="Submit Order"/> </div>

Onesethasadditionalbuttonsthattheusercanclicktonavigatethroughtheproductcategories.TheproblemthatIamworkingaroundisthatjQueryMobilecreatesasetofbuttonswithouttakingintoaccountthevisibilityoftheelementsitisworkingwith.Thatmeanstheouterbuttonsaregivenroundedcornerseveniftheyareinvisible,whichmeansthatusingthevisiblebindingdoesn’tcreatewell-formattedgroupsofbuttons.

TheifbindinghasitsownissuesbecausejQueryMobilewon’tautomaticallyupdatethestylingofbuttonswhennewelementsareaddedtothecontainer,andaskingjQueryMobiletorefreshthecontentdoesn’taddressthisissue.So,thesimplestapproachistocreateduplicatesetsofelements.

UsingTwo-PassDataBindingsDuplicatingelementsisOKforsimplesituations,butitbecomesproblematicwhenyouareworkingwithcomplexsetsofelementsthathavealotofbindingsandformatting.Atsomepoint,achangewillbeappliedtoonesetofelementsandnottheother.Trackingdownthiskindofissuewhenithappenscanbetime-consuming.Analternativeapproachistogenerateduplicatesetsofelementsfromasingletemplate.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>

Page 216: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

216

<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">

Page 217: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

217

<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">&nbsp;</button> <!-- /ko --> <input type="submit" value="Submit Order"/> <!-- ko if: $data --> <button id="right" data-icon="arrow-r" data-iconpos="right">&nbsp;</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>

Page 218: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

218

<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,Ineedtoexplaintheminreverseorderfromhowtheyappearinthedocument.

InvokingaTemplatewithCustomDataIhaveusedthetemplatebindingtogenerateelementsfromaKnockout.jstemplate,atechniquethatIdescribedinChapter3:

<!-- ko template: {name: 'buttonsTemplate', foreach: [true, false] } --> <!-- /ko -->

ThetwististhatIamnotusingtheviewmodeltodrivethetemplate.Instead,Ihavecreatedanarraythatcontainstrueorfalsevalues.Iamapplyingthistechniqueinaverysimplesituation,andIneedtoknowonlyifIamcreatingthesetofbuttonsthatallowforcategorynavigation(representedbythetruevalue)orthesetthatdoesn’t(representedbythefalsevalue).Thepointisthatyoucanusetheforeachbindingwithdatathatisnotpartoftheviewmodel.Youcanusemorecomplexdatastructuresformorecomplexsetsofelements.

UsingaTemplatetoGenerateBindingsThesecondstepisalittleodd.Iusetheattrdatabindingstosetthevalueofthedata-bindattributeontheelementsthataregeneratedbythetemplate,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">&nbsp;</button> <!-- /ko --> <input type="submit" value="Submit Order"/> <!-- ko if: $data --> <button id="right" data-icon="arrow-r" data-iconpos="right">&nbsp;</button> <!-- /ko --> </div> </script>

Page 219: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

219

Thesimplestpartofthetemplateistheuseoftheifbindingtofigureoutwhenthecategorynavigationbuttonsshouldbegenerated.Mytemplatewillbeusedtwice:onceeachforthetrueandfalsevaluesthatIpassedtotheforeachbinding.Whenthevalueistrue,thebuttonelementsareincludedintheDOM,andtheyareomittedwhenthevalueisfalse.

ThemorecomplexpartiswhereIhaveusedtheattrbindingtospecifyavaluethatIwantforthedata-bindattributeintheelementsthataregeneratedbythetemplate.Hereisthevalueofthedata-bindattributeinthetemplate:

data-bind="attr: {'data-bind': 'visible: ' + ($data ? '' : '!') + 'device.smallAndLandscape()'}"

Thereisalotgoingoninthisbinding.ThemostimportantthingtounderstandisthatIamspecifyingthedata-bindvalueIwantthegeneratedelementstohaveasastring,andthisstringwon’tbeprocessedatthemoment.I’llreturntotheprocessingshortly.

Iuse$datatorefertothevaluesIpassedtotheforeachbindingwhenIcalledthetemplate.Thevalueof$datawillbeeithertrueorfalse.First,Knockoutwillresolvethispartofthebinding,sowhenIamdealingwiththetruevalue,thegenerateddivelementwillhaveabindinglikethis:

data-bind="attr: {'data-bind': 'visible: device.smallAndLandscape()'}"

andthefalsevaluewillcauseabindinglikethis:

data-bind="attr: {'data-bind': 'visible: !device.smallAndLandscape()'}"

Then,oncethedatavalueshavebeenresolved,Knockoutwillprocesstheentireattrbinding,whichhastheratherneateffectofreplacingitselfinthegeneratedelement,likethis:

data-bind="visible: device.smallAndLandscape()"

ReapplyingtheDataBindingsKnockoutprocessesthedata-bindattributeonlyonce,whichmeansthatmytemplategenerateselementswiththedatabindingsthatIwant,butthesebindingsarenotlive.Changesintheviewmodelwon’taffectthembecausethedata-bindattributeswerenotdefinedwhenIcalledtheko.applyBindingsmethod.

Tofixthis,IsimplycallapplyBindingsagain,butthistimeIusetheoptionalargumentthatallowsmetospecifywhichelementsareprocessed:

$(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.InowselectallmembersofthisclassandusetheeachmethodtocalltheapplyBindingsmethodoneachelementinturn.Thismakes

Page 220: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

220

Knockout.jsprocessthebindingsthatIgeneratedfromthetemplateandmakethemlive.Thisfinalstepmeansthatmybindingswillrespondtochangesintheviewmodel.

Thereareacoupleofpointstonoteaboutthistechnique.First,IamnottryingtopreventduplicationofelementsintheDOM.ThereisnoeasywaytodealwiththejQueryMobileformattingissueswithoutduplicateelementsets.MygoalistogeneratetheduplicatesfromasinglesetofsourceelementssothatImakechangesinoneplaceandhavethemtakeeffectinalloftheduplicateswhentheyaregenerated.

Second,whenusingthistechnique,youmustensurethatyoudon’trefertoviewmodelitemsexceptwithinapairofquotecharacters(i.e.,withinastring).Ifyourefertoavariableoutsideofastring,thenKockout.jswilltrytofindavaluetoresolvethereference,andyouwillgetanerror.ViewmodelvaluesareresolvedinthesecondcalltotheapplyBindingsmethodandnotwhenthetemplateisusedtocreateelements.

CautionItcanbedifficulttogetthestringproperlysetup,buttheeffortisworthwhileforcomplexsetsofelements.Forsimplersituations,Isuggestyousimplyduplicatewhatyouneedinsidethedocumentandskipthetemplatesaltogether.Thesourcecodedownloadforthisbookcontainsthefulllistingsforthisexample.

AdoptingtheMultipageModelMymobilewebappisshapingup,butIamstillmissingURLrouting,whichmeansthereisasignificantdifferencebetweenthemobileanddesktopversions.Thefirststepinaddingsupportforroutingistoembracethemultipagemodel.AsIexplainedearlier,jQueryMobilesupportstheideaofhavingmultiplepagesinasingleHTMLdocument.Iwillusethisfeaturetoprovidetheuserwiththemeanstonavigatebetweencategories.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>

Page 221: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

221

<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 ? '' : '!')

Page 222: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

222

+ 'device.smallAndLandscape()'}"> <!-- ko if: $data --> <button class="left" data-icon="arrow-l">&nbsp;</button> <!-- /ko --> <input type="submit" value="Submit Order"/> <!-- ko if: $data --> <button class="right" data-icon="arrow-r" data-iconpos="right">&nbsp;</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>

Page 223: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

223

<!-- /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),butthebasicapproachistocreateonepagepercategory.Eachpagecontainsaduplicatesetofnavigationitems,andonlythedetailsofindividualproductsdiffer.Forthemostpart,thechangesaretothedatabindingstocreatethiseffect.Somechanges,however,requiremoreexplanation.

ReworkingCategoryNavigationjQueryMobileusesthesameURL-fragment-basedapproachIemployedinthedesktopversiontonavigatebetweenpages.Forexample,ifthereisadivelementwhosedata-roleattributeissettopageandwhoseidattributeissettomypage,IcangetjQueryMobiletodisplaythatpagebynavigatingtothe#mypagefragment.

ThedifferencefromthedesktopwebappisthatjQueryMobileplacessomeconstraintsonthenamesthatcanbeusedforpages.Iusedthefullcategorynamebefore(suchasBritish Cheese),butspacesareaproblemforjQueryMobile,soIhaveusedtheshortcategorynameinstead(British,forexample).HereisthebindingthatsetsthepageID:

<div data-role="page" data-theme="a" data-bind="attr: {'id': shortName, 'data-category': category}">

NoticethatIhaveaddedadata-categoryattributethatcontainsthefullcategoryname.I’llreturntothisattributeshortly.

ReplacingRadioButtonswithAnchorsThepagenavigationmodelmeansthatIcanreplacemyradiobuttonswithaelements.jQueryMobilewillcreatebuttonwidgetsfromanaelementifthedata-roleattributeissettobutton,andthevalueofthehrefattributecanbeusedfornavigationwithinthedocument:

<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>

Page 224: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

224

Whenthedatabindingsareresolved,Igetanavigationelementwhosepurposeisaloteasiertodivine:

<a data-role="button" href="#British" <span>British</span> </a>

ClickingoneofthebuttonsthatjQueryMobilecreatesfromthiskindofelementwillnavigatetotheappropriatecategorypage.Asanaddedbonus,jQueryMobileproperlycentersgroupsofbuttonscreatedfromaelements,soIdon’thavetoworryaboutexplicitlysettingthewidthofthecontainingfieldsetelement.

TipNoticethatIhaveusedthecssbindingtoapplytheui-btn-activeclasstothebuttonwhentheselectedcategorymatchesthecategorythatbuttonrepresents.ThisisthejQueryMobileCSSclassthatisusedwhenabuttonisactive,andapplyingthisclasscreatesthebluehighlightingthatIhadinthepreviousversionofthemobilewebapp.DiggingaroundinthetoolkitCSSisn’tideal,butsometimesthereisnoalternative.

MappingPageNamestoRoutesSothatIcanreusemyJavaScriptcodeforhandlingroutes,Iwanttousethesameroutenamesasinthedesktopversion.ThisisaproblembecauseoftherestrictionsonpagenamesthatjQueryMobileenforces.Togetaroundthis,IhaveaddedaroutethatmapsbetweentheroutesthatjQueryMobilerequiresandtheroutesIreallywant:

... 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

Page 225: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

225

jQueryMobileURLmatchesthehighlightedroute,andIenumeratetheproductsintheviewmodeltofindtheonethathasamatchingshortNamevalue.IusethecategorypropertyoftheproducttocreatethekindofURLthatthedesktopversionusesandcallthecrossroads.parsemethodtohaveitmatchedagainsttheapplicationroutes.ThistechniqueallowsmetobridgebetweenthejQueryMobileURLsandroutesIwant,allowingmetopreserverouteconsistencyacrossallversionsofmywebapp.Thisisn’tabigdealwithmysimpleexampleroutes,butthisbecomesausefultrickifyouhaveanexternalJavaScriptfilefullofJavaScriptcodethatisexecutedwhenURLsarematched.

ExplicitlyChangingPagesThelastchangerelatestothedata-categoryattributethatIaddedtothepagedivelements.Whentheuserswipesthescreenorusesoneofthelandscapenavigationbuttons,theadvanceCategoryfunctioniscalled,andthevalueoftheselectedCategoryitemintheviewmodelisupdated.However,updatingtheviewmodeldoesn’tautomaticallycausejQueryMobiletonavigatetothepagefortheselectedcategory.Toaddressthis,Ihaveaddedacalltothemobile.changePagemethod.ThismethodwillacceptaURLtonavigatetoorajQueryobjectastheelementtodisplay:

$('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-categoryitemtoselectthepageelementforthenewselectedCategoryvaluewithouthavingtoiteratethroughtheproducts.Withthissmalladdition,IcanrelyonthesameadvanceCategorycodethatIuseinthedesktopversionofthewebappbutgetthebenefitsofthejQueryMobilepagemodel.

AddingtheFinalChromeThereisjustonefinalchangethatIwanttomaketotheCheeseLuxmobileapp.Atonelevel,itisanentirelytrivialchange,butitdoesalsoallowmetodemonstrateanimportantbehavioralquirkthatjQueryMobiledisplays.

jQueryMobileplaysaslidinganimationwhenanewpageisdisplayed.Bydefault,thepageslidesinfromtheright.ThechangethatIwanttomakeistohavethenewpageslideinfromtheleftwhentheuserpressestheleftlandscapenavigationbuttonorpressesoneoftheportrait/largescreenbuttonsforacategorythatappearsintheviewmodelbeforethecurrentcategory.

ThejQueryMobilechangePagemethodacceptsanoptionalconfigurationobject.OneoftheobjectpropertiesthatjQueryMobilerecognizesisreverse.Whenthevalueofthispropertyistrue,thepageappearsfromtheleft.Thedefaultvalue,false,causesthenewpagetoappearfromtheright.

Fortheportraitnavigationbuttons,Ihaveaddedafunctiontoutils.jscalledgetIndexOfCategory.Thisfunction,whichisshowninListing8-11,enumeratesthroughtheviewmodeldatatofindtheindexofaspecifiedfullorshortcategoryname.

Page 226: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

226

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();

Page 227: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

227

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); } }); }); crossroads.parse(location.hash.slice(1)); }); }); }); </script>

IjustneededtoprovidetheoptionalargumenttothechangePagemethodtomakethehorizontalbuttonswork.Fortheaelements,Idecidedtohandletheclickevent,figureoutthetransitiondirection,andcallthechangePagemethoddirectly.ThereareotherwaysofdoingthisinjQueryMobile,butthisisthesimplestandmostdirect.

TheimportantjQueryMobilecharacteristicIwantedtodemonstraterelatestothewaythatinternalURLsaremanaged.jQueryMobilewillnavigatetotheURLfortheentiredocumentratherthanthespecificpageifyouusethechangePagemethodtonavigatetotheURLthatrepresentsthefirstpageinthedocument.Forexample,ifyoucallchangePage('#British'),jQueryMobilewillnavigatetocheeselux.com/mobile.htmlandnotcheeselux.com/mobile.html#British.

Tocaterforthis,IneedtochangetheroutethatmapsbetweenthejQueryMobile–friendlyfragmentURLsandtheroutessharedwiththedesktopversionofthewebapp,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),andifthereisnocategorynameprovidedaspartoftheURL,Iassumethatthefirstcategoryintheviewmodelshouldbeused.Thisisasimplechangeformywebapp,butifyouaremappingcomplexsetsofroutes,youmustensurethatyousetdefaultsforalloftheroutesegmentsthatareexpectedandwouldusuallybeprovidedbythedesktopversion.

Page 228: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER8CREATINGMOBILEWEBAPPS

228

SummaryInthischapter,IcreatedasolidmobileimplementationofmyCheeseLuxwebapp.Ishowedyoutheimportanceofadoptingthenavigationmodelprovidedbythemobiletoolkityouareusingandvariousapproachesforintegratingthecorefeaturesofaprofessional-levelwebapp,suchasrouting,viewmodels,anddatabindings.Mobilewidgettoolkitsusuallyrequiresometweaksandtrickstogetthemtoplaynicelywithprowebapps,buttheresultisworthfiguringoutsolutionstothewrinklesthatarise.Inthenextchapter,IshowyoudifferenttechniquesforimprovingthewayyouwriteandpackageyourJavaScriptcode.

Page 229: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

C H A P T E R 9

229

Writing Better JavaScript

Inthischapter,IexplainsomeofthetechniquesIusetocreatebetterJavaScript.Thisisnotalanguageguide,andIwon’tbedemonstratinganycodehacksortweaks.Mycodingpreferencesareyourmaintenancenightmares,andviceversa.Ihaveseenotherwisemild-manneredpeopleendupinascreamingmatchoverthe“right”waytocode,andIdon’tseethepointinlecturingyouwhenIhaveafairfewbadhabitsmyself.

Instead,IamgoingtoshowyousomeofthetechniquesIusetomakemycodeeasierforotherprogrammersandprojectstouse.Mostlarge-scalewebappshaveateamofprogrammers,andsharingcodebecomesimportant.

Ihavebeendumpingusefulfunctionsintotheutils.jsfilethroughoutthisbook.ThisishowItendtowork,withageneralkitchen-sinkfilewhereIputfunctionsthatIexpecttorepeatedlyuse.Forthisbook,usingutils.jsletmespendmoretimeineachchapteronthetopicsathandwithouthavingtospendpageslistingcodethatIdefinedinapreviouschapter.Italsoletmedemonstratetheideaofusingacoresetofcommonfunctionswhencreatingdesktopandmobileversionsofthesamewebapp.

Theproblemwithjustdumpingfunctionsintoafileinthiswayisthattheybecomehardtomanageandmaintainand,asI’llexplainshortly,difficultforotherstointegrateintotheirprojects.Forthisreason,Irevisitmykitchen-sinkfilewhenIhavereachedapointinaprojectwherethebasicfunctionalityisstableandIhaveagoodfeelforthewaythatdifferentfeaturesfittogether.Atthispoint,andnotbefore,Istarttoreworkthecodeintomodulessothatitplaysnicelywithotherlibraries.Inthischapter,IshowyouthetechniquesIuseforthis.

OnceIhavetidiedupandmodularizedthecode,Ibeginunittesting.Testingisaverypersonalthing,andmanytestingproselytizerswillinsistthattestingmustbeginassoonasyoustartcoding,ifnotsooner.Iunderstandthatpointofview,butIalsoknowthatIdon’teventhinkabouttestinguntilIhavemadeacertainamountofprogresswithaproject.TherenaturallycomesapointwhereIhaveenoughprogressandmymindstartstoturntowardconsolidatingandimprovingwhatIhave.

TestingisanothertopiconwhichIamnotgoingtolecture.Myonlyadviceisthatyoushouldbehonestwithyourself.Testwhenitfeelsright,testuntilyouarehappywithyourcode,andusethetechniquesandtoolsthatworkforyou.Dowhatisrightforyourproject,andacceptthattestinglaterwillrequiremorecodingchangesandthatnottestingatallmeansyouruserswillhavetofindyourbugsforyou.

ManagingtheGlobalNamespaceOneofthebiggestproblemswithlargeJavaScriptprojectsisthelikelihoodofanamingcollision,wheretworegionsofcodeusethesameglobalvariablenamesfordifferentpurposes.Aglobalvariableisonethatexistsoutsideafunctionorobject.JavaScriptmakestheseavailablethroughoutyourwebapplicationsothataglobalfunctiondefinedinaninlinescriptelementorexternalJavaScriptfileis

Page 230: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

230

availabletoeveryotherscriptelementandJavaScriptfileyouuse.Whenaglobalfunctionorvariableiscreated,itissaidtoresideintheglobalnamespace.

Forsmallapplications,thisisausefulfeature;itmeansthatyoucanjustpartitionyourcodeandrelyonthebrowsertomergeittogetherwhentheapplicationisloaded.Thisiswhatallowsmyutils.jsfiletowork:thebrowserloadsallofthefunctionsinmyfileandmakesthemavailableviaglobalvariables.Idon’tneedtoknowwherethemapProductsfunctionisdefinedtouseit;itisautomaticallyavailable.

Theproblemcomeswhenyouusecodethathasfunctionsandvariableswiththesamenamesthatyouhaveused.AllsortsofproblemswillariseifIuseaJavaScriptlibrarythatdefinesamapProductsfunction.ThemapProductscontainedinthefilethatisloadedlastistheonethatwillwin,andanycodethatwasexpectingtheotherversionisgoingtobesurprised.

Whatcanbeausefultrickinasmallwebappbecomesamaintenancenightmareasawebapplicationgrowsinsizeandcomplexity.Itsoonbecomeshardtothinkupmeaningfulnamesthatarenotalreadyinuse,andthelikelihoodofcollisionincreasessharply.Inthesectionsthatfollow,Idescribesomeusefultechniquesthatwillhelpyouavoidnamingcollisionsbystructuringyourcodeandreducingthenumberofglobalvariablesthatarecreatedasaconsequence.

AVOIDING IMPLIED GLOBAL VARIABLES

Acommoncauseofglobalvariablesistoassignvaluestovariablesthathavenotbeendefinedusingthevarkeyword.JavaScriptinterpretsthisasarequesttocreateaglobalvariable:

... (function() { var var1 = "my local variable"; var2 = "my global variable"; })(); ...

Inthislisting,thevariablevar1existsonlywithinthescopeofthefunctionthatdefinesit,butvar2isdefinedintheglobalnamespace.Thiscanbeausefulfeaturewhenusedcarefullyanddeliberately,allowingyoucontroloverwhichvariablesareexportedglobally,butusuallythissituationarisesthrougherrorratherthanintention.Ihaveshownthisinaself-executingfunction,butitcanhappeninanyfunctionthatdefinesvariableswithoutthevarkeyword.

DefiningaJavaScriptNamespaceThefirsttechniqueistoemploynamespaces,whichlimitthescopeofvariablesandfunctions.YouwillbefamiliarwithnamespacesifyouhaveusedalanguagelikeJavaorC#.JavaScriptdoesn’thaveanamespacelanguageconstructlikethoselanguages,butyoucancreatesomethingthatsolvestheproblembyrelyingonthewaythatJavaScriptscopesobjects.Listing9-1showshowthisisdone.

Listing9-1.DefiningaJavaScriptNamespace

var cheeseUtils = {}; cheeseUtils.mapProducts = function(func, data, indexer) { $.each(data, function(outerIndex, outerItem) { $.each(outerItem[indexer], function(itemIndex, innerItem) {

Page 231: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

231

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,Icreateanobjectandthenassignmyfunctionsandvariablesaspropertieswithinit.Thismeansthattoaccessthesefunctionselsewhere,Ihavetousethenameoftheobjectasaprefix,likethis:

cheeseUtils.mapProducts(function(item) { if (item.id == id) { item.quantity(0); } }, cheeseModel.products, "items");

Tobeclear,thisisn’tarealnamespacebecauseJavaScriptdoesn’tsupportthem;itjustlooksandactsalittlebitlikeone.Butitisenoughtoreducepollutionoftheglobalnamespace,inthatIhavetakentwofunctionsoutofthesharedcontextandreplacedthemwithasingleobjectname,cheeseUtils.

Thereisstillariskofnamecollision,soitisimportanttoselectanamefortheobjectthatisspecifictoyourprojectorareaoffunctionality.Youcannestnamespacesbynestingobjects,creatingahierarchythatmustbenavigatedinordertouseyourcode.Listing9-2showsanexample.

TipTosavespace,Iwon’tlistallofthefunctionsthatareintheutils.jsfile.I’lljustpicksomerepresentativesamplestodemonstratethedifferenttechniques.

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;

Page 232: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

232

if (bindingConfig.prefix) { result = bindingConfig.prefix + result; } if (bindingConfig.suffix) { result += bindingConfig.suffix;} return result; }

InthislistingIhaveusedaprettystandardapproachtonamespaces,whichistousethestructureofmydomainnamebutinreverse.However,sincecomislikelytobeusedbyotherlibrariesfollowingthesameapproach,thenIchecktoseewhetherithasbeendefinedalreadybeforedoingsomyself.Idon’thavetodothisforthecheeseluxpartbecauseIamtheownerofthecheeselux.comdomainandthereislittlechanceofcollision.

Referringdirectlytofunctionsinanestednamespacecanleadtoverbosecode.WhenIusethecodeinanestednamespaces,Itendtoaliastheinnermostobjecttoalocalvariable,likethis:

var utils = com.cheeselux.utils;

ThiscreatesalooseequivalenttotheimportorusingstatementsdefinedbyJavaandC#(albeitwithouttheisolationfeaturesthatthoseotherlanguagessupport).

Ilikeusingnestednamespaces,probablybecauseItendtowritemyserver-sidecodeinC#,whichencouragesthesameapproach.Tomakecreatingthenamespacessimpler,Irelyonthefactthatglobalvariablesareactuallydefinedaspropertiesonthewindowbrowserobject.Thismakesiteasytocreatevariablesbynamewithoutrelyinginthedreadedevalfunction,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; }

Page 233: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

233

ThecreateNamespacefunctiontakesanamespaceasanargumentandbreaksitintosegments.Theobjectthatrepresentseachsegmentiscreatedonlyifitdoesn’talreadyexist,whichmeansthatIdon’tcollidewithanyoneelse’suseofcomorwithothercom.cheeselux.*namespacesthatIcreateinseparateJavaScriptfilesformyproject.

■TipCreatingseparatefilesisentirelyoptional.Youcandefinemultiplenamespacesinasinglefileifyouprefer.Theadvantageofasinglefileisthatthebrowserhastomakeonlyonerequesttogetallofyourcode.Ifyoudolikeusingmultiplefiles,thenyoucansimplyconcatenatethemintoonewhenyoureleaseyourwebapp.

Icangoonestepfurtherandmakethenamespaceitselfmoreeasilyconfigurable,asListing9-4demonstrates.ThismakesitmucheasiertorenamemynamespaceifthereisaconflictandmeansthatIcanselectashorternametosavemyselfsometyping.

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.Thisallowsmetocreateanamespaceandassigntheresultasavariable,whichIcanthenusetoadd

Page 234: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

234

functionstothenamespace.IfIneedtochangethenameofthenamespace,thenIhavetodoitonlyinthecalltothecreateNamespacemethod(and,ofcourse,inanycodethatreliesonmyfunctions).Inthisexample,Ihaveshortenedmynamespacebydroppingthecomprefix.Theoddsoftherebeingaconflictarestillprettyslim,butifitdoesarise,itisasimpleenoughmattertoadapt.

UsingSelf-executingFunctionsOnedrawbackoftheprevioustechniqueisthatIendupcreatinganotherglobalvariable,utilsNS.Thisisstillabetterapproachthandefiningallofmyvariablesglobally,butitissomewhatself-defeating.

Icanaddressthisbyusingaself-executingfunction.ThistechniquereliesonthefactthataJavaScriptvariabledefinedwithinafunctionexistsonlywithinthescopeofthatfunction.Theself-executingaspectmeansthatthefunctionrunswithoutbeingexplicitlyinvokedfromanotherpartofthecode.Thetrickistodefineafunctionandhaveitexecuteimmediately.Itiseasiertoseethestructureofaself-executingfunctionwhenthereisn’tanyothercode:

(function() { ...statements go here... })();

Tomakeafunctionself-execute,youwrapitinparenthesesandthenapplyanotherpairofparenthesesattheend.Thisdefinesandcallsthefunctioninasinglestep.Anyvariablesdefinedwithinthefunctionaretidiedupafterthefunctionhasfinishedexecutinganddon’tendupintheglobalnamespace.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;

Page 235: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

235

if (bindingConfig.prefix) { result = bindingConfig.prefix + result; } if (bindingConfig.suffix) { result += bindingConfig.suffix;} return result; } })();

Theonlyglobalvariablethatisleftisthecheeseluxnamespaceobject.Myfunctionsaredefinedwithinthecheeselux.utilsnamespace,andmyutilsNSvariableistidiedupbythebrowserwhentheself-executingfunctionhasfinished.

Consumingafunctiondefinedinthiswayisstilljustamatterofreferringtothefunctionviathenamespace,likethis:

cheeselux.utils.mapProducts(function(item) { if (item.id == id) { item.quantity(0); } }, cheeseModel.products, "items");

CreatingPrivateProperties,Methods,andFunctionsInJavaScript,everyproperty,method,andfunctionisavailableforusefromanyotherpartofthecodethatcreatesorcanaccessthem.Thismakesitdifficulttoindicatewhichmembersareintendedforusebyothersandwhicharetheinternalimplementationsoffeatures.

Thedifferenceisimportant;youwanttobeabletochangetheinternalimplementationtofixbugsoraddnewfeatureswithouthavingtoworryifsomeonehascreatedadependencythatyouweren’texpecting.Anyoneusingyourcodeneedstoknowwhatpropertiesandmethodstheycanrelyonnottochangewithoutduenotice.JavaScriptdoesn’thaveanykeywordsthatcontrolaccess(suchaspublicandprivate,whicharefoundinotherlanguages)andsoweneedtofindalternativeapproachestoaddressthisshortfall.

Thesimplestsolutiontothisproblemistoadoptanamingconventionthatmakesitclearthatsomepropertiesandmethodsarenotintendedforpublicuse.Themostwidelyadoptedconventionistoprefixprivatenameswithanunderscorecharacter(_).

MycomposeStringfunctionisanidealcandidatetobeprivate.Iusethisfunctiononlyinmycustomdatabindings,andIwanttobefreetochangeeveryaspectofthisfunction(includingitsveryexistence)asmybindingsevolve.Thereisnoreasonforanyotherprogrammertodependonthisfunction,eveniftheyusemybindings.Listing9-6showstheunderscorenamingstyleappliedtothisfunctionandthedatabindingsthatrelyonit.

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;

Page 236: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

236

}; 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,butitdoessignalthatdoingsoisagainstthewishesofthedeveloperandthattheproperty,method,orfunctionissubjecttochangewithoutnotice.Itisimportanttouseanamingconventionthatiswidelyadopted(suchastheunderscore)orthatisimmediatelyobvious(suchasprefixingnameswiththewordprivate).

Analternativeapproachistolimitthescopeofprivatefunctionssothattheyarenotdefinedaspartofthenamespace.Thispreventsthefunctionfrombeingaccessedelsewhereinthewebapp,butitmeansthatallofthedependenciesonthatfunctionmustappearwithinthesameself-executingfunction,whichisn’talwayspractical.Listing9-7showshowthisapproachworks.

Page 237: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

237

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())); } } })();

Page 238: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

238

The_composeStringfunctionisneverdefinedaspartofthelocalorglobalnamespacesandisavailableonlyforuseinthesameenclosingself-executingfunction.ThistechniqueworksbecauseJavaScriptsupportsclosures,whichbringsvariablesandfunctionsinscopeevenwhentheyaredefinedinthismanner.

ManagingDependenciesPackagingupmyfunctionsintonamespacesmakesthemmoremanageableandhelpscleanuptheglobalnamespace,butthereisstillonemajorissue:dependenciesonotherlibraries.Inthesectionsthatfollow,Ishowyouatechniqueformanagingdependenciesinlibrariesthatisstartingtogaininpopularityandthatyoucanusetomakeyourcodeeasiertoshareandeasiertoworkwith.

UnderstandingAssumedDependencyProblemsTherearetwokindsofdependencyinanexternalJavaScriptfilesuchasutils.js.Thefirstkindisanassumeddependency,whereIjustusethefunctionalityofalibraryandassumeitwillbeavailable.Ihavedonethisalotinutils.js,especiallywithjQuery.AnassumeddependencyplacesresponsibilityontheHTMLdocumentthatusesaJavaScriptfiletoloadtherequiredlibrariesandtodosobeforemycodeisexecuted.ThemapProductsfunctionisagoodexampleofanassumeddependency:

utilsNS.mapProducts = function(func, data, indexer) { $.each(data, function(outerIndex, outerItem) { $.each(outerItem[indexer], function(itemIndex, innerItem) { func(innerItem, outerItem); }); }); }

ThisfunctionassumesthatthejQuery$.eachmethodwillbeavailable.Ifyouwanttousethisfunction,thenyouneedtoensurethatjQueryisloadedandreadybeforeyoucallmapProducts.Listing9-8showsaverysimplejQueryMobilewebappthatmakesuseofthemapProductsfunction.Thereisnothingnewinthistinywebapp,butIamgoingtouseittodemonstratedifferentdependencyissuesandsolutionsinthesectionsthatfollow.

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>

Page 239: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

239

<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,andclickingthebuttondisplaysthenumberofcheeseswithinthatcategory.Ignore,ifyouwill,thefactthatthereareeasierwaystoobtainthisinformationthanusingthemapProductsmethodandthattherearethreecheesesineverysinglecategory.Thiswitlesswebappisperfectfordemonstratingthekeyaspectsofdependencymanagement.

Page 240: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

240

UnderstandingDirectlyResolvedDependenciesThetinywebappworksbecausejQueryhasbeenloadedlongbeforeIcallthemapProductsfunction.ThesituationwouldbedifferentifIrewrotethewebapptouseadifferenttoolkit.Mostprogrammersdothesamethingwhentheyfirstunderstandthatassumeddependenciesareaproblem:theyassumecontrolofthesituationandtakedirectactiontofixit.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,IhavetakenresponsibilityforresolvingmydependencyonjQuerybyusingModernizrtoloaditbeforecreatingmymapProductsfunction.(TheloadpropertyinaModernizr.loadobjectspecifiesthattheJavaScriptfileshouldalwaysbeloaded.)

Indoingthis,Ihavetransformedanassumeddependencyintoadirectlyresolveddependency.AdirectlyresolveddependencyiswhenIrelyonanotherJavaScriptlibraryandItakedirectactiontomakemycodework,usuallybyloadingthelibrarymyself.

UnderstandingtheProblemsCausedbyResolvingaDependencyDirectlyresolvingadependencycausesalotofproblems.First,IcreatedanassumeddependencyonModernizrtoensurethatjQueryisloaded,whichisn’tahugestepforward.ButtherealdamageisthatIhavemadesurethatthemapProductsfunctionworks;however,indoingso,Ihaveunderminedthestabilityofthewebappitself.

Toseetheproblem,loadthewebapp,andreloadthepageafewtimes.Therearetwoissues.Ifthewebappworks,youhaveencounteredjusttheleastseriousone,whichisthatthejQuerylibraryhasbeenloadedtwice.YoucanseethisinthebrowserdevelopertoolsorintheconsoleoutputfromtheNode.jsserverthatprintsouteachURLthatisrequested.Hereisthelistoffilesloadedbythewebappasreportedbytheserver,withannotationstohighlightthetwoloadsforjQuery:

Page 241: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

241

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 <-- first load 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 <-- second load GET request for /images/ajax-loader.png

Youcantellwhetheryouhaveencounteredonlythefirstproblembecauseyouwillseethreebuttons,andclickingoneofthemmakesamessageappear.Youknowthatyouhaveencounteredthesecondproblemifyoujustgetanemptywindow.Figure9-1showsbothoutcomes.

Figure9-1.Thetwooutcomesthatarisefromadirectlyresolveddependency

Thesecondproblemisaracecondition,anditwon’talwaysmanifestitselfwhenyouareloadingalloftheresourcesfromthewebappfromthelocalmachine.IftheAjaxrequestcompletesafterModernizrhasloadedthejQuerylibraryandexecutedthecallbackfunction,thenyouwillgettheblankwindow,andtherewillbeanerrormessageintheJavaScriptconsolelikethis:

Uncaught TypeError: Cannot call method 'initializePage' of undefined

Theexactwordingwillvaryfrombrowsertobrowser,buttheproblemisthatthecallto$.mobile.initializePagehasfailedbecausethereisno$.mobileobject.Tohelpforcetheproblemtoappear,IhaveaddedaspecialURLtotheNode.jsserverthatintroducesadelayinreturningtheJSONcontent.Totriggerthisdelay,changethenameoftheJSONfilerequestedbythegetJSONmethod,asshowninListing9-10.

1

Page 242: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

242

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-seconddelaytotheAjaxrequestthatwillforcetheAjaxrequesttotakelongerthanModernizrrequirestoloadthejQuerylibrary.Youcanedittheserver.jsfiletoaddalongerdelayifyoudon’tseetheproblem,butone-secondconsistentlycausesthewhitescreenforme.

TipThisispartofwhatmakesthisproblemsonasty;itusuallywon’tappearduringdevelopmentbecausetheAjaxrequestwillcompletesoquickly.Unfortunately,itdoesappearindeploymentwhenrequestsaremadetobusyserversovercongestednetworks.Ifyoueverfindyourselfgettinguserreportsofblankscreensthatyoucan’treplicate,itisalwaysagoodideatoseewhetheryourlibrariesareself-resolvingdependencies.

HereisthesequenceofeventswhentheAjaxrequestcompletesbeforeModernizrhasloadedjQuery:

1. jQueryisloadedbythebrowserfromthescriptelementinexample.html andsetsupthe$shorthandreference.

2. jQueryMobileisloadedandaddsthemobilepropertytothejQuery$shorthand.

3. TheAjaxrequestcompletes,andthe$.mobile.initializePagemethodiscalled.

4. ModernizrloadsthejQuerylibraryagain,whichreplacesthe$shorthandwithanobjectthatdoesn’thavethejQueryMobilemobileproperty.

Page 243: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

243

Thisisthebest-casescenariowherejQueryisloadedandexecutedtwice,butatleastthewebappworks.ThesequencechangeswhentheAjaxrequestcompletesafterModernizrhasloadedjQuery:

1. jQueryisloadedbythebrowserfromthescriptelementinexample.html andsetsupthe$shorthandreference.

2. jQueryMobileisloadedandaddsthemobilepropertytothejQuery$shorthand.

3. ModernizrloadsthejQuerylibraryagain,whichreplacesthe$shorthandwithanobjectthatdoesn’thavethejQueryMobilemobileproperty.

4. TheAjaxrequestcompletes,andthe$.mobile.initializePagemethodiscalled.

Youcanseetheproblem:thecallto$.mobile.initialPageismadeafterthesecondinstanceofjQueryhasbeenloadedandthe$shorthandhasbeenredefined,whicherasesthemobileproperty.TheeffectisthatloadingjQueryasecondtimehasunloadedjQueryMobileandsothewebappdiesahorribledeath.Eveninthebest-casescenario,theonlyreasonthatthewebappworksisbecauseitissosimple;anycalltoajQueryMobilefunctionwillcauseaproblemonceModernizrhascausedthemobileobjecttobedeleted.

TipThereisasecondraceconditioninthissituation.ThemapProductsfunctionisn’tdefineduntilModernizrhasloadedthejQuerylibrary,whichmeansthatadelayinprocessingtherequest(becausetheserverorthenetworkisbusy)canleadtothecodeintheinlinescriptelementcallingmapProductsbeforeitexists.Iamnotgoingtodemonstratethisissue,butyougettheidea:directlyresolveddependenciesareextremelydangerous.

MakingaBadProblemintoaSubtleBadProblemBeforemovingtoarealdependencysolution,Iwanttoshowyouacommonattemptatfixingthedouble-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); }); }); } } }) ...

Page 244: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

244

IhaveusedModernizrtotestsomeindicatorthatjQueryhasalreadybeenloadedandusethenopepropertytoloadtheJavaScriptfileifithasn’t.Applyingthistechniquetomytinyexamplewebappwillmakeeverythingwork.Butitisn’tarealsolution,andwhilethenewproblemIcreatedoccurslessfrequently,itismuchhardertotrackdown.

TheunderlyingproblemisthatIamstilljusttryingtomakemycodework.Ifutils.jsistheonlyfilethatusesthistechnique,theneverythingwillbefine,withtheexceptionthatthemapProductsfunctionmaynotbedefinedinatimelyenoughmannerifthejQuerylibrarydoesneedtobeloadedandthereisadelayintherequest.However,ifthistechniqueisusedinmorethanonefile,thenthereisaverysubtleracecondition.ImaginethattherearetwofilesthatuseModernizrtotestforjQuery:fileA.jsandfileB.js.Mostofthetime,thesequenceofeventswillbethis:

1. ThebrowserexecutesthecodeinfileA.js,whichtestsforjQuery.jQueryhasn’tbeenloaded,soModernizrrequeststhefileandthenexecutesthecompletefunction.

2. ThebrowserexecutesthecodeinfileB.js,whichtestsforjQuery.jQueryhasbeenloadedviafileA.js,andModernizrexecutesthecompletefunctionwithoutneedingtoloadanyfiles.

However,Modernizrrequestsareasynchronous,whichmeansthatthebrowserwillcontinuetoexecuteJavaScriptcodewhileModernizrwaitsfortheresponsefromtheserver.So,ifthetimingisjustright,thesequencewillreallybeasfollows:

1. ThebrowserexecutesthecodeinfileA.js,whichtestsforjQuery.jQueryhasn’tbeenloaded,soModernizrrequeststhefile.

2. ThebrowsercontinuestoexecutecodewhileModernizriswaitingandbeginsprocessingfileB.js.TheModernizrrequestfromfileA.jshasn’tcompletedyet,sofileB.jscausesModernizrtomakeasecondrequestforthejQueryfile.

3. ThefileA.jsrequestcompletes,jQueryisloaded,andthefileA.jscompletefunctionisexecuted.

4. ThefileB.jsrequestcompletes,jQueryisloadedforasecondtime,andthefileB.jscompletefunctionisexecuted.

AnypropertiesthatthecompletefunctioninfileA.jsaddstothejQuery$shorthandwillbelostwhenModernizrloadsjQueryagain.Thissequenceoccursinfrequently,butwhenitdoes,itcankillthewebappbydeletingessentialfunctionalityrequiredinatleastoneoftheJavaScriptfiles.Youmightthinkthatinfrequentproblemsareacceptable,butinfrequentcanstillbeaseriousissuewhenyourwebapphasmillionsofusers.

UsingtheAsynchronousModuleDefinitionTheonlyrealwaytoeliminateraceconditionsandduplicatedlibraryloadingistodealwithdependenciesinacoordinatedway,andthismeanstakingresponsibilityforloadingdependenciesoutofindividualJavaScriptfilesandconsolidatingthem.ThebestmodelfordoingthisistheAsynchronousModuleDefinition(AMD),whichI’llexplainanddemonstrateinthesectionsthatfollow.

Page 245: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

245

DefininganAMDModuleDefiningamoduleisprettysimpleandhingesontheuseofthedefinefunction.Listing9-11showshowIhavecreatedamoduleinanewfilecalledutils-amd.js.Youdon’thavetoincludeamdinthefilename;that’sjustmypreferencebecauseIliketomakeitasobviousaspossibletotheconsumersofmycodethattheyaredealingwithAMD.ProvidingthedefinefunctionistheresponsibilityoftheAMDloader.AsanauthorofAMDmodules,youcanrelyonthedefinefunctionbeingpresentwithouthavingtoworryaboutwhichloaderisbeingusedorhowthefunctionisimplemented.

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.Thefirstargumentisanarrayofthelibrariesthatthecodeinthemoduledependson.Thesecondargumentisafunction,knownasthefactoryfunction,thatcontainsthemodulecode.OnlyoneAMDmodulecanbedefinedinafile,andsinceIliketokeepthefunctionalitydefinedinamodulenarrowlyfocused,myutils-amd.jsfilecontainsjustthemapProductsandcomposeString functions.(I’llreturntosomeoftheothercodefromutils.jsinawhile.)

AnAMDmodulecanrelyonallofthedeclareddependenciesbeingloadedbeforethefactoryfunctionisexecuted.Inthiscase,Ihavedeclaredadependencyonjquery-1.7.1.js,andIcanassumethatthisJavaScriptfilewillbeloadedandjQuerywillbeavailableforusewhenIsetupmymapProductsandcomposeStringfunctions.TheresultfromthefactoryfunctionisanobjectwhosepropertiesarethefunctionsIwanttoexportforuseelsewhereinthewebapp.AnyvariablesorfunctionsthatIdefineandthatarenotpartoftheresultobjectwillbetidiedupwhenthefactoryfunctionhasexecutedwithoutpollutingtheglobalnamespace.

TipNoticethatthereisnonamespaceinmymodule.OneofthenicefeaturesofAMDisthatitisuptotheconsumerofmymoduletodecidehowtorefertothefunctionalitythatIdefine,asI’lldemonstrateinthenextsection.

Page 246: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

246

UsinganAMDModuleAMDsolvesthedependencyissuesbyhavingasingleresourceloadertakeresponsibilityforloadinglibraries.Thisloaderisresponsibleforexecutingamodule’sfactoryfunctionandensuringthatthelibrariesitreliesonareloadedandreadybeforethishappens.Themainmeansofcommunicationbetweenamoduleandtheloaderisthroughthedefinefunction,whichtheloaderisresponsibleforimplementing.

Bystandardizingtheloadingprocess,thedecisionaboutwhichloadertouseislefttotheconsumerofAMDmodules,ratherthantheauthor.So,Idon’thavetoworryaboutresolvingdependencieswhenIwriteanAMDmodule,andIdon’tevenhavetoworryabouthowtheywillbedealtwith.

AlthoughtheAMDformatisgainingpopularity,notallresourceloaderssupportAMD.ThisincludesModernizr.load,whichIhavebeenusingtoloadlibrariessofarinthisbook(andtodemonstratewhythisisabadideainthischapter).MyfavoriteAMD-awareloaderisrequireJS,whichyoucandownloadfromhttp://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);

Page 247: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

247

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>

DeclaringDependenciesThefirstthingtodoisremoveallofthescriptelementsintheheadsectionofthedocumentandreplacethemwithasingleelementthatimportsrequireJS.ThisensuresthatrequireJShasacompleteviewofallofthedependenciesinthewebappandthatyoudon’tenduploadingscriptfilestwiceiftheyarerequiredindependentlibraries.

... <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',

Page 248: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

248

'modernizr-2.0.6.js' ]; require(libs, function(utils, device) { ...

ThemostimportantfeatureofanAMDloaderistherequirefunction,whichisthecounterparttodefine.Therequirefunctiontakestwoarguments:anarrayofmodulesandscriptfilesthatthewebappdependsonandacallbackfunctiontoexecutewhentheyareallloaded.Ifindthatdefiningthedependencyarrayasavariablemakesmycodemorereadable,butthatispurelyapersonalpreference.

NoteTheAMDmoduletakescareoftheproblemsaroundhowdependenciesareresolved,butitstillrequiresthattheJavaScriptfilesareavailablefromthewebserver.Whensharingyourcodewithothers,youwillstillneedtoletthemknowwhichlibrariesyoudependuponandmakeitclearthatyouareusingAMDandsotheywillneedanAMDloader.

Noticethatsomeoftheitemsinthedependencyarrayhavea.jssuffixandothersdon’t.NotallofthedependenciesorawebappwillbewrittenasAMDmodules.IfyoupassrequireJSthenameofaJavaScriptfile(i.e.,witha.jssuffix),thenitwillloadthefileandexecutethecodeinsideofitjustlikeanyregularresourceloader.

Ifyouomitthe.jssuffix,thenrequireJSassumesyouhavespecifiedanAMDmoduleandactsaccordingly.Itwilladdthe.jssuffixwhenitrequeststhefilefromtheserver,andwhenitreceivestheresponse,itwilllookforthedefinefunctioninordertodiscoverthedependenciesandthefactoryfunction.

TipByforcingeachfiletocontainonlyonemodule,AMDincreasesthenumberofHTTPrequeststhatarerequiredtogetthescriptsforawebapp.Inthisexample,Ihavegonefromonefile(utils.js)tothree(utils-amd.js,device-amd.js,andcustombindings-amd.js).IwouldhaveendedupwithmoreifIhadproperlypackagedupallofthefunctionsthatutils.jscontained.Toaddressthis,requireJSsupportsaserver-sideoptimizerthatwillconcatenatemultipleAMDmodulefilesintoasingleresponse.Seehttp://requirejs.org/docs/optimization.htmlfordetails.

DealingwithCallbackArgumentsForeachAMDmoduleinthelistpassedtorequire,thereisacorrespondingargumentpassedtothecallbackfunction.Eachargumentissettotheobjectreturnedbythefactoryfunctioninthemodule.Thisisanicealternativetonamespaces;theconsumerofthemodulegetstodecidehowtorefertothemodulefunctionsratherthanthecreator.

Thefirstmoduleinmylistisutils-amd,andthiscorrespondstotheutilargumentinmycallbackfunction.WhenIwanttousethemapProductsfunctiondefinedbythemodule,Imakeacalllikethis:

Page 249: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

249

utils.mapProducts(function(inner, outer) { if (outer.category == e.currentTarget.id) { count++; } }, cheeseModel.products, "items") cheeseModel.selectedCount(count);

IfIlaterstartusingaregularJavaScriptlibrarythatusesutilsasaglobalvariable,IcaneasilychangethewaythatIrefertothecodeintheutils-amdmodulebyrenamingtheargumentforthecallbackfunction.And,sincethefunctionsarescopedwithinthecontextofthecallbackargument,AMDmodulesdon’tpollutetheglobalnamespaceatall.

So,whyaretherethreeAMDmodulesinthelistbutonlytwocallbackarguments?Theansweristhatmodulesarenotrequiredtoreturnanobjectiftheydon’tneedtoexportfunctions,andthisistheapproachIhavetakenwiththecustombindings-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,andtherearenonewfunctionstoexportdirectlyfromthemoduleforuseelsewhere.

Page 250: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

250

TipNoticethatthecustombindings-amdmoduledependsontheutils-amdmodule.TheAMDloaderisresponsibleforensuringthatallthedependenciesareresolved,whichmakesreusingmodulesverysimple.

Therequirecallbackfunctiondoesreceiveanargumentwhenamodulethatdoesn’treturnanobjectisloaded,butthevalueofthatargumentisnull.So,Icouldeasilyhavewrittenmycallbackfunctionlikethis:

require(libs, function(utils, device, bindings) { ... }

Butthereislittlepointbecausethebindingsobjectwillbenull.Theorderoftheargumentsalwaysreflectstheorderofthemodulesintherequirelist,soIalwaysputthemodulesthatdon’treturnobjectsattheendofthelistsothatIcanomitthenullargumentsthatcorrespondtothem.

DeclaringInlineDependenciesItisn’talwayspossibletodeclareallofthedependenciesatthestartofascriptblock.Asanexample,inordertopreventjQueryMobilefromautomaticallyprocessingthedocument,IneedtoloadjQueryandsetupaneventhandlerbeforethejQueryMobilelibraryisloaded.Youcansimplycalltherequirejsfunctiontodeclaredependencieswithinarequirestatement,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,Iamabletodeclaremydependencieswithouthavingtoloadallofthecodefilesatonce.ThisgrantsmespacebetweenjQueryandjQueryMobilebeingloadedinwhichIcansetupmyeventhandler.

ThisisalsothetechniqueIhaveusedinthedevice-amdmoduletoreplacetheModernizr.loadmethod.Listing9-14showsthecodefromChapter7whereIloadapolyfillbasedonthepresenceofabrowserfeature.

Page 251: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

251

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,loadingthedependencyandthecallbackfunctionsoelegantly.TherequireJSequivalentisshowninListing9-15,whichshowsthedevice-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();

Page 252: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

252

$(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’tsufferfromtheproblemsIdescribedearlierinthechapter.Ifyouareworkingonalargeprojectorsharingcodewithothers,thenasingle,coordinatedapproachtodependencesisessential,evenifthecodestyleisn’tquiteassmooth.

Page 253: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

253

UnitTestingClient-SideCodeThelasttopicthatIwanttocoverinthisbookisunittesting.Thetoolsforunittestingwebappsarenotassophisticatedasthosefordesktoporserver-sidecode,buttheyarestillprettygood,andyouwillfinditeasytoembraceclient-sideunittestingaspartofyourdevelopmentcycle—ifyouareabelieverinunittesting,anyway.

AsIsaidatthebeginningofthischapter,Iamnotgoingtolectureyouabouttheimportanceoftestingortellyouwhenyoushouldbegintestingyourcode.Frommyownexperience,Iresistedunittestingforalongtime,inpartbecauseofthenumberofzealotsthatkeptinsistingthattestingbedoneatacertaintimeandinacertainway.Thesedays,Ihavecometoseethevalueinunittesting,butwhenandhowunittestingisbestappliedvariesfromprojecttoprojectandprogrammertoprogrammer.Iamabigbelieverinwritingbetter-qualitycode,butIhaveanintensedislikeforrigidapproachesthattreateverysituationinthesameway.

Withthatinmind,Iamgoingtobrieflyintroduceyoutotheclient-sidetestingtoolthatIliketouseandthenleaveyoutofigureouthowtoapplyit.Likeallofthetechniquesinthisbook,youshouldpickwhatworksforyou,adapteverythingtoyourownneeds,andsimplyignoreanythingthatdoesn’tsolveanyproblemsyouarefacing.

UsingQUnitIuseQUnit,whichisthetooldevelopedbythejQueryteamfortheirunittesting.Itissimpleandeffectiveandworkswell.YoucangetQUnitfromhttp://github.com/jquery/qunit.ToinstallQUnit,downloadtheQUnitpackageandcopythequnit.jsandqunit.cssfilesfromthequnitfolderinthearchivetotheNode.jscontentfolder.

QUnittestsarerunfromanHTMLdocument,andthereisabasicstructureofelementsrequiredinthisdocumentsothatQUnitcandisplaythetestresults.Listing9-16showsthetemplatethatIusedwhentestingAMDmodules,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>

Page 254: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

254

<h2 id="qunit-userAgent"></h2> <ol id="qunit-tests"></ol> <div id="qunit-fixture">test markup, will be hidden</div> </body> </html>

TouseQUnit,ensurethatthescriptandCSSfilesyoucopiedintothecontentdirectoryareimportedintothedocument.

ForeachmoduleIwanttotest,IusetheQUnitmodulefunctiontodenotethestartofaseriesoftestsanduserequireJStoloadthemodulecode.(TheQUnitmodulefunctionisn’trelatedtoAMDmodules;itjustgroupstogetherasetofrelatedtestsintheoutputdisplay.)

ThemarkupaddedtothetemplateallowsQUnittodisplaytheresults.Youcanchangethemarkuptoformatyourresultsdifferently,andinformationaboutthemeaningofeachelementcanbefoundathttp://docs.jquery.com/QUnit,alongwiththefullAPIdocumentation.

IhaveaddedjQuerytomylistofscriptimports,butQUnitdoesn’trequirejQuerytorun.IfindjQueryusefulforcreatingmorecomplextests,asI’lldemonstrateshortly.

TipBecarefulifyouareusingrequireJStoloadQUnit.TheQUnitlibraryinitializesitselfinresponsetotheloadeventonthewindowbrowserobject,andthiseventisusuallytriggeredbeforerequireJShasloadedthejQuerylibraryandexecutedthecallbackfunction.IfyouabsolutelymustuserequireJS,thenyoucanmakeacalltoQUnit.load()intherequireJScallbackfunction.

AddingTestsforaModuleWiththebasicstructureinplace,Icanbegintoaddtestsformymodule.IamgoingtokeepthingssimpleandperformsomeargumenttestsonthecomposeStringfunction,makingsurenullargumentsdon’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"

Page 255: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

255

}; 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,withargumentsforthenameofthetestandafunctionthatcontainsthetestcode.IneachofthefourtestsIhaveadded,Icreateanobjectwiththeprefix,suffix,andvaluepropertiesthatarepassedtomyfunctionviamycustomdatabindingsandpassthistothecomposeStringfunction,whichIaccessthroughtheutilsargumenttomyrequireJScallbackfunction,likethis:

equal(utils.composeString(config), "prefixvalue");

Likemostunittestpackages,QUnitprovidesaseriesofassertionsthattesttheresultofanoperation.Inthiscase,IhaveusedtheequalfunctiontocheckthattheresultfromcallingthecomposeStringfunctionmatchesmyexpectation.Arangeofdifferentassertionsareavailable,andyoucanseethefulllistathttp://docs.jquery.com/QUnit.

Toruntheunittests,simplyloadtests.htmlintothebrowser.QUnitwillperformeachtestinturnandusethemarkupasacontainerfortheresults.MycomposeStringfunctionpassesoneofthetestsandfailstheothertwo.Theresultsaredisplayedinthebrowser,asshowninFigure9-2.

Page 256: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

256

Figure9-2.ExecutingunittestsonthecomposeStringfunction

ThereisabuginthecomposeStringfunction,whichdoesn’tchecktoseewhetherthevaluepropertyoftheobjectpassedastheargumentexistsorhasbeenassignedavalue.Tofixthisproblem,ImakethechangeshowninListing9-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.Mysimplefixresolvestheproblemwiththetwobrokentests,andreloadingtests.htmlgivesmetheall-clear.

Page 257: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

257

UsingjQuerytoPerformTestsonHTMLIamnotgoingtowriteacompletesetoftestsformymodulesbecauseQUnitbehavesjustlikeanyotherunittestpackage,exceptitoperatesonJavaScriptinthebrowser,especiallyforself-containedfunctionslikecomposeStringwheretheinputandtheresultareallexpressedinJavaScript.

However,aslightlydifferentapproachisrequiredwhentheeffectorresultofthecodebeingtestedisexpressedinHTML.ThisisthereasonthatIincludedjQueryinmyQUnittesttemplate,andtodemonstratethistechnique,IwillwritesometestsfortheformatAttrbindinginthecustombindings-amdmodule,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,andtestfragmentsofHTMLwithoutneedingtoaddthemtothedocument.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 = {

Page 258: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

258

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.IfyoupassanHTMLfragmenttothejQuery$shorthandfunction,theresultisaDOMAPIelementthatisnotattachedtothedocument.Asabonus,Idon’thavetomakesurethatthesingleanddoublequotesinthedata-bindattributeareproperlyescapedwhenusingthejQueryattrmethod:

var testElem = $("<a></a>").attr("data-bind", "formatAttr: {attr: 'href', prefix: '#', value: cat}")[0];

NoticethatIusedanarray-styleindexertogetthefirstelementintheobjectreturnedbythejQuery$shorthandfunction.Theko.applyBindingsmethodworksontheDOMAPIobjectratherthanjQueryobjectsandsoIneedtounwraptheaelementIhavecreatedfromthejQueryobject.Atthispoint,IcangetKnockout.jstoapplybindingstomyHTMLfragmentusingmytestviewmodel:

ko.applyBindings(viewModel, testElem);

Totesttheresult,IusetheQUnitequalfunctionandboththeDOMAPIandjQuerytoinspecttheresult:

equal(testElem.attributes.length, 2); equal($(testElem).attr("href"), "#British");

jQuerymakesiteasytocreateandprepareHTMLfortestingandchecktheresults,andasthisexampleshows,youcanusetheDOMAPItogetinformationabouttheelementsafterthetesthascompleted.Asyoucansee,jQueryandQUnittogethermaketestingeveryaspectofawebapppossibleand,forthemostpart,easytodo.

Page 259: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CHAPTER9WRITINGBETTERJAVASCRIPT

259

SummaryInthischapter,IshowedyouthetoolsandtechniquesIusetowritebetterJavaScript,notbetterinthesenseofamorecompleteuseofthelanguagefeaturesbutbetterinthesenseofeasierforotherstoworkwith,easierformetomaintain,and,withtheapplicationofunittesting,sotheuserwillexperiencefewerproblems.Thesetechniques,combinedwiththosefromearlierchapters,giveyouasolidfoundationonwhichtobuildscalable,dynamic,andflexiblewebappsthatareeasytouseandeasytomaintain.Goodluckonallofyourprojects,andremember,asIsaidinChapter1,thatanythingworthdoingontheserversideisworthconsideringfortheclientside,too.

Page 260: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

261

Index

A

Ajaxrequests,129addingAjaxGETrequest,130–131addingAjaxURLtomainmanifest,135addingAjaxURLtomanifestNETWORK

section,135errorhandling,133–134POSTrequestbehavior,135products.jsonFile,131restructuring,131–133

Applicationcacheentries,122Applicationcachespecification,126Asynchronousmoduledefinition(AMD)

callbackarguments,248–250definition,245dependencydeclaration,247–248dependencyissues,246–247factoryfunction,245inlinedependencies,250–252

B

Bidirectionalbindings,58–60

C

Cache-controlheader,122CheeseLux,9–12

addingrouting,101–105browser,105enhancingviewmodel,106managingapplicationstate,107–108mapProductsfunction,106

Clickevent,25–26Code

fragment,6HTMLdocument,5

Contentdistributionnetwork(CDN),16CSSclass,22–24

D

DataStorage,browser.SeeHTML5localstoragefeature

Defaultactionsmanagement,27–29Designpatterns,4Desktopwebbrowser,7Dynamicbasket,29

E

Emptybasket,71–74Event,definition,24

F

Fallbackentries,122–126Flowcontrolbindings,52–53$function,18–19

G

Graphicdesignandlayouts,4

H

Hovermethod,jQuery,26HTMLeditor,7HTML5HistoryAPI

preservingviewmodelstate,96–97restoringapplicationstate,99–101storingapplicationstate,98–99

HTML5localstoragefeature,137–139complexdatastorage(seeIndexedDB)withformelements,143–144forJSONdata,139–141withobjects,141–142withofflinewebapplications

addingbuttons,154–155

Page 261: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

INDEX

262

HTML5localstoragefeature,withofflinewebapplications(cont.)

cachedCheeseLuxwebapp,150–153createDialogfunction,156–157enhanceViewModelfunction,153–154scriptelementchanges,155–156

persistentforms,142–143sessionstorage

benefits,148–149semi-persistentobservabledataitem,149

synchronizingviewmodels,144KOsubscribemethod,146maindocumentmodification,147–148persistentObservablefunction,144,146–

147StorageEventobject,145

HTMLElementproperties,35

I

IndexedDB,156DBOobject,158–160locatingobjects

usingcursor,166usingIndex,167bykey,165–166

onupgradeneededproperty,160–161successoutcomes,161towebapplication,162–165WebSQL,157workingprinciple,157

J

JavaScriptdependenciesinlibraries,238

AMDmodule(seeAsynchronousmoduledefinition(AMD))

assumeddependency,238–239directlyresolveddependency,240double-loadingproblem,243–244issues,240–243

globalnamespaces,229–230globalvariables,230namespaces

configuration,233–234definition,230–231nested,231–232nested,usingafunction,232–233self-executingfunction,234–235

namingcollision,229property,methodandfunction,235–238unittesting,253

addingtests,254–256jQuery,257–258QUnit,253–254

JavaScriptlibraries,7JavaScriptpolyfilllibraries,129jQuery

addClassmethod,23bindmethod,changeandkeyupevents,32–

33customselectors,20hovermethod,26importing,15–18methodchaining,23methodsforinsertingelementsindocument,

22statement,19UIbutton,43–44UItoolkit,42–43

jQueryMobilecontentchanges,214–215eventsequence,211–212

disablingautomaticprocessing,212–213pageinitevent,213–214

pages,201–202widgets,202

K

Knockout(KO)databindings,51definition,49library,49–53

koobject,50

L

Latentcontent,29,31–32

M

Methodchaining,23Methodpairs,23Mobilebrowseremulator,7MobileWebApps,195

CheeseLuxMobileWebAppbasicimplementation,209formatTextdatabinding,210–211

Page 262: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

INDEX

263

initialversion,206–209duplicatingelementsusingtemplates,215

withcustomdata,218data-bindattribute,218–220usingtwo-passdatabindings,215–218

getIndexOfCategoryfunction,226goals,205jQueryMobile

askmobile.htmldocument,198CheeseLuxwebapp,204dataattributes,201events,203installation,201setCookie,203

mobiledevicedetectioncapabilities,197–198useragent,195–196

multipagemodeladdingsupport,220–223changePagemethod,225mappingpagenamestoroutes,224–225navigation,223replacingradiobuttonswithanchors,223

Mouseenterandmouseleaveevents,26–27

N

Node.js,8

O

Offlinewebapps,109Ajaxrequests

addingAjaxGETrequest,129–131addingAjaxURLtomainmanifest,135addingAjaxURLtomanifestNETWORK

section,135errorhandling,133–134POSTrequestbehavior,135products.jsonfile,131restructuring,131–133

HTML5applicationcacheacceptingchangestomanifest,115–116addingmanifesttoHTMLdocument,

112–113addingnetworkandfallbackentries,122–

126cachedcontent,113–114controlofcacheupdateprocess,116–122manifestfile,111–112

monitoringaddingelementsandbindings,128detectingstateofnetwork,126–128

POSTrequestbehavior,135reviseddocument,109–111

P, Q

Polyfill,98POSTrequestbehavior,135

R

RecurringAjaxrequestspolyfills,129ResponsiveWebApps,169

screenorientation,184–188screensize

adaptingsourcedata,183adaptingwebapplayout,179–183conditionaljQueryUIstyling,183–184CSSmediaqueries,173–174detectDeviceFeaturesfunction,177–178imageloading,178–179JavaScriptmediaqueries,174–175matchMediafeature,176–177polyfill,175–176removingelements,184

touchinteractionapplicationroutes,193detectingtouchsupport,189–190navigation,191–193touchSwipelibrary,190–191

viewport,169–172

S, T

Singlehandlerfunction,27Submitbuttonupgradation

CSSclass,22–24$function,18–19inputelementselectionandhiding,19–21jQuery,15–18newelementinsertion,21–22

U

URLroutingCheeseLux

addingrouting,101–104

Page 263: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

INDEX

264

URLrouting,CheeseLux(cont.)browser,104enhancingviewmodel,106managingapplicationstate,107–108mapProductsfunction,106

consolidatingroutesaddingdefaultroute,89–90optionalsegments,88–89unexpectedsegmentvalues,86–88variablesegments,85–86

event-drivencontrolstonavigationbridgingeventsandrouting,92–94bridgingURLroutingandJavaScript

events,90–92selecteddatabinding,94

usingHTML5HistoryAPIhistory.replaceStatemethod,95preservingviewmodelstate,96–97restoringapplicationstate,99–101stepsdemonstratingtheissue,95storingapplicationstate,98–99

simpleroutedwebapplication,77–78addingnavigationmarkup,81–83addingroutinglibrary,79addingviewmodelandcontentmarkup,

79–81applyingcontrolsandelements,83–85

V

Valuebindings,51–52Viewmodel,47

addingmoreproducts,53–54dynamicbasket

addingbasketlineitems,66–69addingbasketstructureandtemplate,70–

71addingsubtotals,64–66emptybasket,71–74removingitems,71

generatingcontent,61–63modelcreation

addingdatatodocument,48

adoptingviewmodellibrary,49bidirectionalbindings,58–60extendingthemodel.60–61generatingcontent,49–53observabledataitems,55–58

resetting,47–48reviewing,63–64URLrouting,79–80

W, X, Y, Z

Webappdevelopmentprinciples,15dynamicbasketdata

addingbasketelements,29–31bindmethod,changeandkeyupevents,

32–33changingformtarget,37–39latentcontent,31–32overalltotalcalculation,35–37subtotalcalculation,33–34subtotaldisplay,34–35

eventhandlingclickevent,25–26defaultactions,28–29usingeventobject,27mouseenterandmouseleave,26–27singlehandlerfunction,27

JavaScriptdisabledandenabled,39–40JavaScript-onlypolicy,41non-JavaScriptusers,40submitbuttonupgradation

CSSclass,22–24$function,18–19inputelementselectionandhiding,19–21jQuery,15–19newelementinsertion,21–22

UItoolkitcreatingjQueryUIbutton,43–44settingupjQueryUI,42

Webserver,7whitelistentries,122WURFLdatabase,195

Page 264: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

ProJavaScriptforWebApps

AdamFreeman

Page 265: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

ii

ProJavaScriptforWebApps

Copyright©2012byAdamFreeman

Thisworkissubjecttocopyright. 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 terdeveloped.Exemptedfromthislegalreservationarebriefexcerptsinconnectionwithreviewsorscholarlyanalysisormaterialsuppliedspecificallyforthepurposeofbeingenteredandexecutedonacomputersystem,forex clusiveusebythepurchaserofthework.DuplicationofthispublicationorpartsthereofispermittedonlyundertheprovisionsoftheCopyrightLawofthePublisher'slocation,ini tscurrentversion,andpermissionforusemustalwaysbeobtained fromSpringer.PermissionsforusemaybeobtainedthroughRightsLinkattheCopyrightClearanceCenter.ViolationsareliabletoprosecutionundertherespectiveCopyrightLaw.

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 everyoccurrenceofatrademarkedname,logo,orima geweusethe names,logos,and imagesonlyina neditorialf ashionandtothebenefitofthetrademarkowner,withnointentionofinfringementofthetrademark.

Theuseinthis publicationof tradenames,tr ademarks, servicema rks,a nds imilarterms,ev enifth eya re notidentifiedassuch,isnottobeta kenasanexpres sionofopinion astowhet herornottheyaresubjecttoproprietary rights.

Whiletheadviceandinf ormationinthis bookarebelievedtobe trueandaccurateatthedateof publication,neithertheauthorsnor theeditorsnorth epublishercanacceptanylegal responsibilityforanyerrorsoro missionsthatmaybemade.Thepublishermakesnowarranty,expressorimplied,withrespecttothematerialcontainedherein.

PresidentandPublisher:PaulManningLeadEditor:BenRenow-ClarkeDevelopmentEditor:LouiseCorriganTechnicalReviewer:RJOwenEditorialBoard: Ste veAn glin, EwanBuckin gham,Gary Corn ell,Louise Corrigan ,Morgan Erte l,Jon athan

Gennick,JonathanHassell, RobertHutchin son, MichelleLowman,JamesMarkh am,MatthewM oodie,JeffOlson,J effreyP epper,D ouglas Pundick,Ben R enow-Clarke,D ominicSha keshaft,G wenanSp earing,M attWade,TomWelsh

CoordinatingEditor:JenniferL.BlackwellCopyEditor:KimWimpsettCompositor:BythewayPublishingServicesIndexer:SPiGlobalArtist:SPiGlobalCoverDesigner:AnnaIshchenko

DistributedtothebooktradeworldwidebySpringerScie nce+BusinessMediaNewYork, 233SpringStreet,6thFloor,New York,NY 10013.Phone 1-800-SPRINGER,f ax(20 1)348 -4505, e-mail [email protected],orv isitwww.springeronline.com.

Forinformationontranslations,[email protected],orvisitwww.apress.com.

Apressand friendsofEDbook smaybe purchasedinbulkf oracademic,corporate,orpromo tionaluse.eBoo kversionsandlicensesarealsoavailableformostti tles.Formoreinformation,referenceourSpecialBulkSales–eBookLicensingwebpageatwww.apress.com/bulk-sales.

Anysourcecodeorothersupplementary materialsref erencedbytheauthori n thiste xtisav ailabletore adersatwww.apress.com.Fordetailed inf ormationabouthowtoloca teyourbook’ssourcecode, goto www.apress.com/source-code.

Page 266: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

iii

Dedicatedtomylovelywife,JacquiGriffyth.

Page 267: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

v

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

Page 268: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CONTENTS

vi

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

Page 269: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CONTENTS

vii

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

Page 270: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CONTENTS

viii

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

Page 271: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CONTENTS

ix

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

Page 272: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CONTENTS

x

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

Page 273: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

CONTENTS

xi

AddingTestsforaModule.................................................................................................................... 254

UsingjQuerytoPerformTestsonHTML............................................................................................... 257

Summary .......................................................................................................................259

Index ...................................................................................................................261

Page 274: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

xii

About the Author

AdamFreemanisanexperiencedITprofessionalwhohasheldseniorpositionsinarangeofcompanies,mostrecentlyservingaschieftechnologyofficerandchiefoperatingofficerofaglobalbank.Nowretired,hespendshistimewritingandrunning.

Page 275: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

xiii

About the Technical Reviewer

RJOwenistheleadexperienceplanneratEffectiveUI,focusingoncustomerinsightwork,includingethnographicresearch,designvalidation,co-creationexercises,andexpertdesign.RJstartedhiscareerasasoftwaredeveloperandspenttenyearsworkinginC++,Java,andFlexbeforemovingtothedesignresearchandcustomerinsightteamatEffectiveUI.Hetrulylovesgooddesignandunderstandingwhatmakespeopletick.RJholdsanMBAandabachelor’sinphysicsandcomputerscience.Heisafrequentspeakeratmanyindustryevents,includingWeb2.0,SXSW,andAdobeMAX.

Page 276: Freeman FM-1 final · Client-sidewebappdevelopmenthasalwaysbeenthepoorcousintoserver ... Thisisanadvancedbook,andyouneedtobeanexperiencedwebprogrammerto ... youneedforeachtopic,butifyouwantabettergroundinginhowjQueryworksand

xiv

Acknowledgments

IwouldliketothankeveryoneatApressforworkingsohardtobringthisbooktoprint.Inparticular,IwouldliketothankJenniferBlackwellforkeepingmeontrackandBenRenow-Clarkeforcommissioningandeditingthistitle.Iwouldalsoliketothankmytechnicalreviewer,RJOwen,whoseeffortsmadethisbookfarbetterthanitwouldhavebeenotherwise.