You are on page 1of 37

Chrome packaged apps

Codelab at Google I/O 2013


Findthisguideonlineathttp://goo.gl/UHCS8
Instructor: RenatoMangini,DeveloperProgramsEngineer TeachingAssistants: EijiKitamura,DeveloperAdvocate JoeMarini,DeveloperAdvocate MikeTsao,SoftwareEngineer SriramSaroop,ProgramManager

Introduction Prerequisites Howtousethismaterial Step1Createandrunaminimumapp HowtodebugaChromepackagedapp Step2ImportandadaptToDoMVCapp CSPCompliance ConvertingfromlocalStoragetochrome.storage.local Step3Alarmsandnotifications:remindyourselfofopenToDo's Part1Alarms: Part2Notifications: Step4ParseURLsandopentheninaWebview Step5Addimagesfromtheweb Step6ExportToDo'stothefilesystem Advancedbonuschallenge:Addvoicecommands

Introduction
WelcometotheChromepackagedappscodelab!Thisisaselfpacedcodelabwhereyouwill exercisethebasicconceptsofaChromepackagedappandalsolearnsomeofitsmanyAPIs. Mostconceptscoveredinthistutorialaredescribedindetailsintheplatform'sofficial documentation.IneachCodelabstepyouwillfindlinkstothespecificAPIscoveredthere. InthefirstchapteryouwilllearnthebasicbuildingblocksrequiredforaChromepackaged applicationandhowtorunanddebugit. Inthesecondchapterwewillbootstrapfromawellknownopensourcewebapplication,the ToDoMVC,andlearnhowtoadaptittorunasaChromepackagedapp,removingunsupported featureslikelocalStorageandachievingCSPcompliance. Fromthethirdchapteronwards,wewilladdfeaturestotheToDoMVCappusingtheChrome packagedappsexclusiveAPIs. Attheend,youshouldhavebuildanofflineenabled,installableandfeaturerichversionofthe ToDoMVCapp.Alongtheway,youshouldlearn: ThebuildingblocksandthebasicdevelopmentflowfortheChromeplatform HowtopackageexistingwebappsandrunitintheChromeplatform chrome.storage.localstorage,anasynchronouslocalobjectstore alarmsandnotificationsAPIs Howtousethewebviewtagtoshowexternal,unrestrictedwebcontent Howtoloadresources,likeimages,fromexternalsources UsingtheextendedFileSystemAPItowritetoafileinthenativefilesystem

Prerequisites
Thiscodelabassumesyouarefamiliarwithfundamentalwebprogramming.Youshouldknow thebasicsofHTMLandCSS,andyoushouldbefamiliarwithJavaScript. YoushouldhaveChromeDevinstalled.Accesschrome://versioninyourChromeandcheckif the"GoogleChrome"isversion28orlater.Ifitisnot,pleasefollowtheinstructionsinthelink https://www.google.com/intl/en/chrome/browser/index.html?extra=devchannel#eula FORPIXELUSERS:ifyouwanttouseyourshinynewPixelChromebook(youshould!),just followtheinstructionsatthebeginningofStep1. IfyoureallywanttouseChrome27,pleasebeawarethatthenotificationsonstep3willnot work.

How to use this material


Startfreshinanewdirectory. Eachstepbuildsontopoftheprevious. Thereisarecommendedtimeforeachstep.Ifyougetpastthisrecommendedtimeanddecide tomoveover,everystephasa"cheat"linkwhereyoucangrabthereferencecodefromthe previousstep.

Downloadthereferencecodeforallstepsfrom:

https://github.com/mangini/io13codelab/archive/master.zip
Installandplaywiththeappathttp://goo.gl/qNNUX

Step 1 - Create and run a minimum app


Objectives:understandthedevelopmentflow Recommendedtimetocompletethisstep:10minutes Besidesthecodespecifictoyourapp,aChromePackagedApprequirestwofiles: Amanifest1 ,whereyouspecifythemetainformationofyourapp Aneventpage2 ,alsocalledbackgroundpage,whereyouusuallyregisterlistenersfor specificappevents,likealaunchlistenerthatopenstheapp'swindow WanttouseyourshinynewChromebookPixel?Prepareitbyfollowingoneoftheflows below: Option1,RECOMMENDEDforthisCodelab: a. Installasimpletexteditorapplikethis3 b. CreateanewfolderundertheDownloadsdirectory c. Optionalbuthighlyrecommended:switchtothenewer,unstableChromeOS. Ifyoudon'tfollowthisstep,youwillhavethestableversion(ChromeOS26) andsomeAPIscoveredinthisCodelabmaynotwork. i. MakesureyouareloggedinastheowneroftheChromebook(thefirst usertosignin) ii. Gotochrome://help/ iii. ClickinMoreinfo... iv. select"Devunstable"inthe"Channel"listbox.Ifyoudon'tseethis listbox,youarenotloggedinastheChromebookowner. v. waituntilitupdates.Arestartisrequiredattheend.Ifyouwantto restorethestableversionaftertheCodelab,youwillneedtofollow theseprocedures. Option2,forADVANCEDusersonly!FollowthestepcfromtheOption1.Andthen switchtoDevelopermodeanduseanyLinuxeditor.SwitchingintoDeveloperMode doesalotofstuff(erasingyourstatefulpartition,disablingsomeofthechecks thatmakesureyou'rerunningofficialcode,etc).Ifyoustillwanttogothisroute aftersomanywarnings,switchtodevelopermode,installcroutonanduseVIMorany Linuxtexteditor.

Openyourfavoritecode/texteditorandcreatethefollowingfilesinanemptydirectory: manifest.json:
1 2

http://developer.chrome.com/trunk/apps/manifest.html http://developer.chrome.com/trunk/apps/app_lifecycle.html#eventpage 3 http://goo.gl/MjR7R

{ " m a n i f e s t _ v e r s i o n " : 2 , " n a m e " : " I / O C o d e l a b " , " v e r s i o n " : " 1 " , " p e r m i s s i o n s " : [ ] , " a p p " : { " b a c k g r o u n d " : { " s c r i p t s " : [ " b a c k g r o u n d . j s " ] } } , " m i n i m u m _ c h r o m e _ v e r s i o n " : " 2 8 " }

background.js: c h r o m e . a p p . r u n t i m e . o n L a u n c h e d . a d d L i s t e n e r ( f u n c t i o n ( ) { c h r o m e . a p p . w i n d o w . c r e a t e ( ' i n d e x . h t m l ' , { i d : ' m a i n ' , b o u n d s : { w i d t h : 6 2 0 , h e i g h t : 5 0 0 } } ) } )

index.html: < ! D O C T Y P E h t m l > < h t m l > < h e a d > < m e t a c h a r s e t = " u t f 8 " > < / h e a d > < b o d y > < h 1 > H e l l o , l e t ' s c o d e ! < / h 1 > < / b o d y > < / h t m l >

Congratulations,you'vejustcreatedanewChromepackagedapp.Ifyou'vecopiedfromthe cheatsdirectory,makesureyoualsocopytheicon_128.pngbecauseitisreferredat manifest.json. Nowyoucanrunit:

How to debug a Chrome packaged app


IfyouarefamiliarwiththeChromeDeveloperTools,youwillliketoknowthatyoucanusethe Developertoolstoinspect,debug,auditandtestyourappjustlikeyoudoitonaregularweb page. Theusualdevelopmentcycleis: Writeapieceofcode Reloadtheapp(rightclick,ReloadApp) Test CheckDevToolsconsoleforanyerror Rinseandrepeat.

TheDeveloperToolsconsolehasaccesstothesameAPIsavailabletoyourapp.Thisway,you caneasilytestanAPIcallbeforeaddingittoyourcode:

Step 2 - Import and adapt ToDoMVC app


Wanttostartfreshfromhere?Getpreviousstep'scodeins o l u t i o n _ f o r _ s t e p 1 subfolder! Objectives: LearnhowtoadaptanexistingapplicationfortheChromeappsplatform Learnthechrome.storage.localAPI4 Preparethebasisforthenextcodelabsteps. Recommendedtimetocompletethisstep:20minutes Wewillstartbyimportinganexistingregularwebappintoourproject,andasastartingpoint,we willuseacommonbenchmarkapp,theToDoMVC5 : 1. We'veincludedaversionoftheToDoMVCappinthereferencecodezip.Copyall content,includingsubfolders,fromthet o d o m v c folderintheziptoyourapplication folder.Youshouldnowhavethefollowingfilestructureinyourapplicationfolder: m a n i f e s t . j s o n (fromstep1) b a c k g r o u n d . j s (fromstep1) i c o n _ 1 2 8 . p n g (optional,fromstep1) i n d e x . h t m l (fromtodomvc) b o w e r . j s o n (fromtodomvc) b o w e r _ c o m p o n e n t s / (fromtodomvc) j s / (fromtodomvc) 2. Reloadyourappnow.YoushouldseethebasicUI,butnothingelsewillwork.Ifyouopen theconsole(rightclick,InspectElement,Console),thereisanerrorlike:
Refused to execute inline script because it violates the following Content Security Policy directive: "default-src 'self' chrome-extension-resource:". Note that 'script-src' was not explicitly set, so 'default-src' is used as a fallback. index.html:42

CSP Compliance
1. Let'sfixthiserrorbymakingtheappCSPcompliant6 .OneofthemostcommonCSP noncompliancesiscausedbyinlineJavaScript,likeeventhandlersasDOMattributes (< b u t t o n o n c l i c k = ' ' > )and< s c r i p t > tagswithcontentinsidetheHTML.The solutionissimple:justmovetheinlinecontenttoanewfile. a. editindex.htmlandmovetheinlineJavaScripttoanewfilej s / b o o t s t r a p . j s : < s c r i p t > / / B o o t s t r a p a p p d a t a w i n d o w . a p p = { } < / s c r i p t >
4 5

http://developer.chrome.com/trunk/apps/storage.html IfyouknowtheToDoMVCwebapp(http://todomvc.com),wehavecopieditsvanillaJavaScriptversionto beusedasastartingpoint. 6 http://developer.chrome.com/trunk/apps/app_csp.html

< s c r i p t s r c = " j s / b o o t s t r a p . j s " > < / s c r i p t > b. createaj s / b o o t s t r a p . j s filewiththefollowingcontents: / / B o o t s t r a p a p p d a t a w i n d o w . a p p = { } 3. Reloadyourapp.Notworkingyet,right?ButnowifyouopenConsole,thepreviouserror shouldbegone,butthereisanotherone:
Uncaught window.localStorage is not available in packaged apps. Use chrome.storage.local instead. platformApp:16

4. Thisonetakesmorestepstofix...

Converting from localStorage to chrome.storage.local


LocalStorageisnotsupportedinChromepackagedapps.Why?LocalStorageissynchronous, andsynchronousaccesstoblockingresources(I/O)inasinglethreadedruntimeingeneralis notagreatidea.Ifyourstoragehashighlatency,yourappmightbecomeunresponsive. Chromepackagedappshasanasynchronousequivalentthatcanalsostoreobjectsdirectly, avoidingthesometimescostlyobject>string>objectserializationprocess. Tofixthisinourapp,wewillneedtoconvertlocalStoragetochrome.storage.local.Thereare manysteps,buttheyareallsmallchangestotheAPIcallsorfixesontheToDoMVCasync support. NOTE:changingalltheplaceswerelocalStorageisusedintheToDoMVCis timeconsuminganderrorprone,althoughrequiredandimportantforyourlearning.To maximizeyourfunatthisCodelab,wehighlysuggestthatyou: a. lookatthecodechangesdescribedbelow.Checkifyouunderstandthemand askteachingassistantsanyquestionsifmayhave b. copyfilesfromcheat_code/solution_for_step_2andproceedtothenext CodelabStep.We'vekeptallthestepsbelowforlearningpurposes. 1. Inmanifest.json,requestthe"storage"permission: . . . " p e r m i s s i o n s " : [ " s t o r a g e " ] , . . . 2. Instore.js,fixtheconstructor: f u n c t i o n S t o r e ( n a m e , c a l l b a c k ) { v a r d a t a v a r d b N a m e 9

c a l l b a c k = c a l l b a c k | | f u n c t i o n ( ) { } d b N a m e = t h i s . _ d b N a m e = n a m e i f ( ! l o c a l S t o r a g e [ d b N a m e ] ) { d a t a = { t o d o s : [ ] } l o c a l S t o r a g e [ d b N a m e ] = J S O N . s t r i n g i f y ( d a t a ) } c a l l b a c k . c a l l ( t h i s , J S O N . p a r s e ( l o c a l S t o r a g e [ d b N a m e ] ) ) c h r o m e . s t o r a g e . l o c a l . g e t ( d b N a m e , f u n c t i o n ( s t o r a g e ) { i f ( d b N a m e i n s t o r a g e ) { c a l l b a c k . c a l l ( t h i s , s t o r a g e [ d b N a m e ] . t o d o s ) } e l s e { s t o r a g e = { } s t o r a g e [ d b N a m e ] = { t o d o s : [ ] } c h r o m e . s t o r a g e . l o c a l . s e t ( s t o r a g e , f u n c t i o n ( ) { c a l l b a c k . c a l l ( t h i s , s t o r a g e [ d b N a m e ] . t o d o s ) } . b i n d ( t h i s ) ) } } . b i n d ( t h i s ) ) }

3. Fixthefindmethod: S t o r e . p r o t o t y p e . f i n d = f u n c t i o n ( q u e r y , c a l l b a c k ) { i f ( ! c a l l b a c k ) { r e t u r n } v a r t o d o s = J S O N . p a r s e ( l o c a l S t o r a g e [ t h i s . _ d b N a m e ] ) . t o d o s c a l l b a c k . c a l l ( t h i s , t o d o s . f i l t e r ( f u n c t i o n ( t o d o ) { f o r ( v a r q i n q u e r y ) { r e t u r n q u e r y [ q ] = = = t o d o [ q ] } } ) ) c h r o m e . s t o r a g e . l o c a l . g e t ( t h i s . _ d b N a m e , f u n c t i o n ( s t o r a g e ) { v a r t o d o s = s t o r a g e [ t h i s . _ d b N a m e ] . t o d o s . f i l t e r ( f u n c t i o n ( t o d o ) { f o r ( v a r q i n q u e r y ) { r e t u r n q u e r y [ q ] = = = t o d o [ q ] 10

} } ) c a l l b a c k . c a l l ( t h i s , t o d o s ) } . b i n d ( t h i s ) ) } 4. FixthefindAllmethod: S t o r e . p r o t o t y p e . f i n d A l l = f u n c t i o n ( c a l l b a c k ) { c a l l b a c k = c a l l b a c k | | f u n c t i o n ( ) { } c a l l b a c k . c a l l ( t h i s , J S O N . p a r s e ( l o c a l S t o r a g e [ t h i s . _ d b N a m e ] ) . t o d o s ) c h r o m e . s t o r a g e . l o c a l . g e t ( t h i s . _ d b N a m e , f u n c t i o n ( s t o r a g e ) { c a l l b a c k . c a l l ( t h i s , s t o r a g e [ t h i s . _ d b N a m e ] . t o d o s ) } . b i n d ( t h i s ) ) } 5. Thesavemethodpresentsanewchallenge:sinceitdependsontwoasyncoperations (getandset)thatoperateonthewholemonolithicJSONstorageeverytime,anybatch updateonmorethanoneToDo,like"markallToDosascompleted",willresultinadata hazardknownasReadAfterWrite7 .Thereareseveralwaystofixit,butwewillusethe chancetoslightlyrefactoritbytakinganarrayofToDoid'stobeupdatedatonce.Notice thatthisissuewouldn'thappenifwewereusingamoreappropriatedatastorage,like IndexedDB.Butwearetryingtominimizetheconversioneffort: S t o r e . p r o t o t y p e . s a v e = f u n c t i o n ( i d , u p d a t e D a t a , c a l l b a c k ) { c h r o m e . s t o r a g e . l o c a l . g e t ( t h i s . _ d b N a m e , f u n c t i o n ( s t o r a g e ) { v a r d a t a = J S O N . p a r s e ( l o c a l S t o r a g e [ t h i s . _ d b N a m e ] ) v a r d a t a = s t o r a g e [ t h i s . _ d b N a m e ] v a r t o d o s = d a t a . t o d o s c a l l b a c k = c a l l b a c k | | f u n c t i o n ( ) { } / / I f a n I D w a s a c t u a l l y g i v e n , f i n d t h e i t e m a n d u p d a t e e a c h p r o p e r t y i f ( t y p e o f i d ! = = ' o b j e c t ' | | A r r a y . i s A r r a y ( i d ) ) { v a r i d s = [ ] . c o n c a t ( i d ) i d s . f o r E a c h ( f u n c t i o n ( i d ) { f o r ( v a r i = 0 i < t o d o s . l e n g t h i + + ) { i f ( t o d o s [ i ] . i d = = i d ) { f o r ( v a r x i n u p d a t e D a t a ) { t o d o s [ i ] [ x ] = u p d a t e D a t a [ x ]
7

http://en.wikipedia.org/wiki/Hazard_(computer_architecture)#Read_After_Write_.28RAW.29

11

} } } } ) l o c a l S t o r a g e [ t h i s . _ d b N a m e ] = J S O N . s t r i n g i f y ( d a t a ) c a l l b a c k . c a l l ( t h i s , J S O N . p a r s e ( l o c a l S t o r a g e [ t h i s . _ d b N a m e ] ) . t o d o s ) c h r o m e . s t o r a g e . l o c a l . s e t ( s t o r a g e , f u n c t i o n ( ) { c h r o m e . s t o r a g e . l o c a l . g e t ( t h i s . _ d b N a m e , f u n c t i o n ( s t o r a g e ) { c a l l b a c k . c a l l ( t h i s , s t o r a g e [ t h i s . _ d b N a m e ] . t o d o s ) } . b i n d ( t h i s ) ) } . b i n d ( t h i s ) ) } e l s e { c a l l b a c k = u p d a t e D a t a u p d a t e D a t a = i d / / G e n e r a t e a n I D u p d a t e D a t a . i d = n e w D a t e ( ) . g e t T i m e ( ) t o d o s . p u s h ( u p d a t e D a t a ) l o c a l S t o r a g e [ t h i s . _ d b N a m e ] = J S O N . s t r i n g i f y ( d a t a ) c a l l b a c k . c a l l ( t h i s , [ u p d a t e D a t a ] ) c h r o m e . s t o r a g e . l o c a l . s e t ( s t o r a g e , f u n c t i o n ( ) { c a l l b a c k . c a l l ( t h i s , [ u p d a t e D a t a ] ) } . b i n d ( t h i s ) ) } } . b i n d ( t h i s ) ) }

6. Wealsoneedtochangetheclientofthesavemethod,soitcanincludeallIDsinone call. a. UpdatemethodtoggleCompleteincontroller.js: C o n t r o l l e r . p r o t o t y p e . t o g g l e C o m p l e t e = f u n c t i o n ( i d s , c h e c k b o x , s i l e n t ) { v a r c o m p l e t e d = c h e c k b o x . c h e c k e d ? 1 : 0 t h i s . m o d e l . u p d a t e ( i d s , { c o m p l e t e d : c o m p l e t e d } , f u n c t i o n ( ) { i f ( i d s . c o n s t r u c t o r ! = A r r a y ) { i d s = [ i d s ] } i d s . f o r E a c h ( f u n c t i o n ( i d ) { 12

v a r l i s t I t e m = $ $ ( ' [ d a t a i d = " ' + i d + ' " ] ' ) i f ( ! l i s t I t e m ) { r e t u r n } l i s t I t e m . c l a s s N a m e = c o m p l e t e d ? ' c o m p l e t e d ' : ' ' / / I n c a s e i t w a s t o g g l e d f r o m a n e v e n t a n d n o t b y c l i c k i n g t h e c h e c k b o x l i s t I t e m . q u e r y S e l e c t o r ( ' i n p u t ' ) . c h e c k e d = c o m p l e t e d } ) i f ( ! s i l e n t ) { t h i s . _ f i l t e r ( ) } } . b i n d ( t h i s ) ) } b. UpdatemethodtoggleAllincontroller.js: C o n t r o l l e r . p r o t o t y p e . t o g g l e A l l = f u n c t i o n ( e ) { v a r c o m p l e t e d = e . t a r g e t . c h e c k e d ? 1 : 0 v a r q u e r y = 0 i f ( c o m p l e t e d = = = 0 ) { q u e r y = 1 } t h i s . m o d e l . r e a d ( { c o m p l e t e d : q u e r y } , f u n c t i o n ( d a t a ) { v a r i d s = [ ] d a t a . f o r E a c h ( f u n c t i o n ( i t e m ) { i d s . p u s h ( i t e m . i d ) t h i s . t o g g l e C o m p l e t e ( i t e m . i d , e . t a r g e t , t r u e ) } . b i n d ( t h i s ) ) t h i s . t o g g l e C o m p l e t e ( i d s , e . t a r g e t , f a l s e ) } . b i n d ( t h i s ) ) t h i s . _ f i l t e r ( ) } 7. Let'snowfixtwominorbugsintheToDoMVCcodethatonlyshowupwhenactualasync storageisused: a. Incontroller.js,methodremoveItem,movethe_filtercalltoinsidethecallback: C o n t r o l l e r . p r o t o t y p e . r e m o v e I t e m = f u n c t i o n ( i d ) { t h i s . m o d e l . r e m o v e ( i d , f u n c t i o n ( ) { 13

t h i s . $ t o d o L i s t . r e m o v e C h i l d ( $ $ ( ' [ d a t a i d = " ' + i d + ' " ] ' ) ) t h i s . _ f i l t e r ( ) } . b i n d ( t h i s ) ) t h i s . _ f i l t e r ( ) } b. Alsoincontroller.js,make_updateCountmethodasync: C o n t r o l l e r . p r o t o t y p e . _ u p d a t e C o u n t = f u n c t i o n ( ) { v a r t o d o s = t h i s . m o d e l . g e t C o u n t ( ) t h i s . m o d e l . g e t C o u n t ( f u n c t i o n ( t o d o s ) { t h i s . $ t o d o I t e m C o u n t e r . i n n e r H T M L = t h i s . v i e w . i t e m C o u n t e r ( t o d o s . a c t i v e ) t h i s . $ c l e a r C o m p l e t e d . i n n e r H T M L = t h i s . v i e w . c l e a r C o m p l e t e d B u t t o n ( t o d o s . c o m p l e t e d ) t h i s . $ c l e a r C o m p l e t e d . s t y l e . d i s p l a y = t o d o s . c o m p l e t e d > 0 ? ' b l o c k ' : ' n o n e ' t h i s . $ t o g g l e A l l . c h e c k e d = t o d o s . c o m p l e t e d = = = t o d o s . t o t a l t h i s . _ t o g g l e F r a m e ( t o d o s ) } . b i n d ( t h i s ) ) } c. AndthecorrespondingmethodgetCountinmodel.jsnowneedstoaccepta callback: M o d e l . p r o t o t y p e . g e t C o u n t = f u n c t i o n ( c a l l b a c k ) { v a r t o d o s = { a c t i v e : 0 , c o m p l e t e d : 0 , t o t a l : 0 } t h i s . s t o r a g e . f i n d A l l ( f u n c t i o n ( d a t a ) { d a t a . e a c h ( f u n c t i o n ( t o d o ) { i f ( t o d o . c o m p l e t e d = = = 1 ) { t o d o s . c o m p l e t e d + + } e l s e { t o d o s . a c t i v e + + } t o d o s . t o t a l + + } ) i f ( c a l l b a c k ) c a l l b a c k ( t o d o s ) } ) 14

r e t u r n t o d o s } 8. Wearealmostthere.Ifyoureloadtheappnow,youwillbeabletoinsertnewTodo's,but youwon'tbeabletoremovethem.Let'snowfixtheremovemethodinstore.js.The samedatahazardissuementionedinthesavemethodisalsopresenthere,soweneed tochangetheremovemethodtoallowforbatchoperations: a. Fixtheremovemethodinstore.js: S t o r e . p r o t o t y p e . r e m o v e = f u n c t i o n ( i d , c a l l b a c k ) { c h r o m e . s t o r a g e . l o c a l . g e t ( t h i s . _ d b N a m e , f u n c t i o n ( s t o r a g e ) { v a r d a t a = J S O N . p a r s e ( l o c a l S t o r a g e [ t h i s . _ d b N a m e ] ) v a r d a t a = s t o r a g e [ t h i s . _ d b N a m e ] v a r t o d o s = d a t a . t o d o s v a r i d s = [ ] . c o n c a t ( i d ) i d s . f o r E a c h ( f u n c t i o n ( i d ) { f o r ( v a r i = 0 i < t o d o s . l e n g t h i + + ) { i f ( t o d o s [ i ] . i d = = i d ) { t o d o s . s p l i c e ( i , 1 ) b r e a k } } } ) l o c a l S t o r a g e [ t h i s . _ d b N a m e ] = J S O N . s t r i n g i f y ( d a t a ) c a l l b a c k . c a l l ( t h i s , J S O N . p a r s e ( l o c a l S t o r a g e [ t h i s . _ d b N a m e ] ) . t o d o s ) c h r o m e . s t o r a g e . l o c a l . s e t ( s t o r a g e , f u n c t i o n ( ) { c a l l b a c k . c a l l ( t h i s , t o d o s ) } . b i n d ( t h i s ) ) } . b i n d ( t h i s ) ) } b. NowchangetheremoveCompletedItemsincontroller.jstomakeitcall removeItemonallidsatonce: C o n t r o l l e r . p r o t o t y p e . r e m o v e C o m p l e t e d I t e m s = f u n c t i o n ( ) { t h i s . m o d e l . r e a d ( { c o m p l e t e d : 1 } , f u n c t i o n ( d a t a ) { v a r i d s = [ ] d a t a . f o r E a c h ( f u n c t i o n ( i t e m ) { i d s . p u s h ( i t e m . i d ) t h i s . r e m o v e I t e m ( i t e m . i d ) } . b i n d ( t h i s ) ) t h i s . r e m o v e I t e m ( i d s ) } . b i n d ( t h i s ) )

15

t h i s . _ f i l t e r ( ) } c. AndfinallychangetheremoveItemincontroller.jstosupportremovingmultiple itemsfromDOMatonce: C o n t r o l l e r . p r o t o t y p e . r e m o v e I t e m = f u n c t i o n ( i d ) { t h i s . m o d e l . r e m o v e ( i d , f u n c t i o n ( ) { v a r i d s = [ ] . c o n c a t ( i d ) i d s . f o r E a c h ( f u n c t i o n ( i d ) { t h i s . $ t o d o L i s t . r e m o v e C h i l d ( $ $ ( ' [ d a t a i d = " ' + i d + ' " ] ' ) ) } . b i n d ( t h i s ) ) t h i s . _ f i l t e r ( ) } . b i n d ( t h i s ) ) } 9. Youaredone.Reloadyourapp(rightclick,ReloadApp)andenjoy! Thereisanothermethodinstore.jsusinglocalStorage:drop.Butsinceitisnotbeingused anywhereintheproject,wewillleaveitasanexerciseforyoutofixitlater. YoushouldnowhaveacoolworkingChromepackagedversionoftheToDoMVCasinthe screenshotbelow:

16

Gotaproblem?
RemembertoalwayshavetheconsoleofDeveloperToolsopentoseeJavaScripterror messages: OpentheDeveloperTools(rightclickontheapp'swindow,clickonInspectelement) SelecttheConsoletab Youshouldseeanyruntimeerrormessagesthere

17

Step 3 - Alarms and notifications: remind yourself of open To Do's


Wanttostartfreshfromhere?Getpreviousstep'scodeins o l u t i o n _ f o r _ s t e p 2 subfolder! Objectives: learnhowtowakeyourappatspecifiedintervalsusingalarms learnhowtousenotificationstodrawuserattentiontosomethingimportant Recommendedtimetocompletethisstep:20minutes NowwewillchangetheapptoremindyouifyouhaveopenToDo's,evenwhentheappis closed.TheappwillusetheAlarmsAPItosetawakeupintervaland,aslongasChromeis running,thealarmlistenerwillbecalledatapproximatelytheintervalset.

Part 1 - Alarms:
1. Inmanifest.json,requestthe"alarms"permission: . . . " p e r m i s s i o n s " : [ " s t o r a g e " , " a l a r m s " ] , . . . 2. Inbackground.js,addaonAlarmlistenerthat,fornow,justsendalogmessagetothe consolewheneverthereisaToDoiteminthestorage: . . . c h r o m e . a p p . r u n t i m e . o n L a u n c h e d . a d d L i s t e n e r ( f u n c t i o n ( ) { c h r o m e . a p p . w i n d o w . c r e a t e ( i n d e x . h t m l ' , { i d : ' m a i n ' , b o u n d s : { w i d t h : 6 2 0 , h e i g h t : 5 0 0 } } ) } ) c h r o m e . a l a r m s . o n A l a r m . a d d L i s t e n e r ( f u n c t i o n ( a l a r m ) { c o n s o l e . l o g ( " G o t a n a l a r m ! " , a l a r m ) } )

3. Inindex.html,addan"Activatealarm"buttonandimportthescriptwewillcreateinthe upcomingstep: . . . < f o o t e r i d = " i n f o " > < b u t t o n i d = " t o g g l e A l a r m " > A c t i v a t e a l a r m < / b u t t o n > < p > D o u b l e c l i c k t o e d i t a t o d o < / p > . . . < s c r i p t s r c = " j s / s t o r e . j s " > < / s c r i p t > 18

< s c r i p t s r c = " j s / m o d e l . j s " > < / s c r i p t > < s c r i p t s r c = " j s / v i e w . j s " > < / s c r i p t > < s c r i p t s r c = " j s / c o n t r o l l e r . j s " > < / s c r i p t > < s c r i p t s r c = " j s / a p p . j s " > < / s c r i p t > < s c r i p t s r c = " j s / a l a r m s . j s " > < / s c r i p t > < / b o d y > < / h t m l > 4. Createanewscriptjs/alarms.js: addcheckAlarm,createAlarm,cancelAlarmandtoggleAlarmmethods: ( f u n c t i o n ( ) { ' u s e s t r i c t ' v a r a l a r m N a m e = ' r e m i n d m e ' f u n c t i o n c h e c k A l a r m ( c a l l b a c k ) { c h r o m e . a l a r m s . g e t A l l ( f u n c t i o n ( a l a r m s ) { v a r h a s A l a r m = a l a r m s . s o m e ( f u n c t i o n ( a ) { r e t u r n a . n a m e = = a l a r m N a m e } ) v a r n e w L a b e l i f ( h a s A l a r m ) { n e w L a b e l = ' C a n c e l a l a r m ' } e l s e { n e w L a b e l = ' A c t i v a t e a l a r m ' } d o c u m e n t . g e t E l e m e n t B y I d ( ' t o g g l e A l a r m ' ) . i n n e r T e x t = n e w L a b e l i f ( c a l l b a c k ) c a l l b a c k ( h a s A l a r m ) } ) } f u n c t i o n c r e a t e A l a r m ( ) { c h r o m e . a l a r m s . c r e a t e ( a l a r m N a m e , { d e l a y I n M i n u t e s : 0 . 1 , p e r i o d I n M i n u t e s : 0 . 1 } ) } f u n c t i o n c a n c e l A l a r m ( ) { c h r o m e . a l a r m s . c l e a r ( a l a r m N a m e ) } f u n c t i o n d o T o g g l e A l a r m ( ) { c h e c k A l a r m ( f u n c t i o n ( h a s A l a r m ) { i f ( h a s A l a r m ) { c a n c e l A l a r m ( ) } e l s e { 19

c r e a t e A l a r m ( ) } c h e c k A l a r m ( ) } ) } $ $ ( ' # t o g g l e A l a r m ' ) . a d d E v e n t L i s t e n e r ( ' c l i c k ' , d o T o g g l e A l a r m ) c h e c k A l a r m ( ) } ) ( )

NOTES: Observetheparametersinc h r o m e . a l a r m s . c r e a t e call.Thesesmallvalues (0.1ofaminute)arefortestingonly.Inapublishedapp,thesevaluesarenot acceptedandareroundedtoapproximately1minute.Inyourtestenvironment,a simplewarningisissuedtotheconsoleyoucanignoreit. Sincethelogmessageisbeingsenttotheconsoleintheevent(background) page(seestep2above),youneedtoinspectthatbackgroundpagetoseethelog messages: OpentheDeveloperTools(rightclickontheapp'swindow,clickon InspectBackgroundpage,selecttheConsoletab).Wheneveryouhave thealarmactivated,youshouldseelogmessagesbeingprintedinthe consoleeverytimethealarm"rings".

Part 2 - Notifications:
Nowlet'schangethealarmnotificationtosomethingtheusercaneasilynotice.Forthat purpose,wewillusethechrome.notificationsAPI.Wewillshowadesktopnotificationlikethe onebelowand,whentheuserclicksonthenotification,wewillopenorraisetheToDowindow tothetop.

1. Inmanifest.json,requestthe"notifications"permission: . . . " p e r m i s s i o n s " : [ " s t o r a g e " , " a l a r m s " , " n o t i f i c a t i o n s " ] , . . . 20

2. Inbackground.js: movethechrome.app.window.createcalltoamethodsowecanreuseit: . . . f u n c t i o n l a u n c h ( ) { c h r o m e . a p p . w i n d o w . c r e a t e ( ' i n d e x . h t m l ' , { i d : ' m a i n ' , b o u n d s : { w i d t h : 6 2 0 , h e i g h t : 5 0 0 } } ) } c h r o m e . a p p . r u n t i m e . o n L a u n c h e d . a d d L i s t e n e r ( f u n c t i o n ( ) { c h r o m e . a p p . w i n d o w . c r e a t e ( ' i n d e x . h t m l ' , { i d : ' m a i n ' , b o u n d s : { w i d t h : 6 2 0 , h e i g h t : 5 0 0 } } ) } ) c h r o m e . a p p . r u n t i m e . o n L a u n c h e d . a d d L i s t e n e r ( l a u n c h ) . . . createashowNotificationmethod(noticethatthesamplecodebelowreferstoan i c o n _ 1 2 8 . p n g .Ifyouwantyournotificationtohaveanicon,remembertoalso copyitfromthecheatcodeorcreateyourownicon):

. . . v a r d b N a m e = ' t o d o s v a n i l l a j s ' f u n c t i o n s h o w N o t i f i c a t i o n ( s t o r e d D a t a ) { v a r o p e n T o d o s = 0 i f ( s t o r e d D a t a [ d b N a m e ] . t o d o s ) { s t o r e d D a t a [ d b N a m e ] . t o d o s . f o r E a c h ( f u n c t i o n ( t o d o ) { i f ( ! t o d o . c o m p l e t e d ) { o p e n T o d o s + + } } ) } i f ( o p e n T o d o s > 0 ) { / / N o w c r e a t e t h e n o t i f i c a t i o n c h r o m e . n o t i f i c a t i o n s . c r e a t e ( ' r e m i n d e r ' , { t y p e : ' b a s i c ' , i c o n U r l : ' i c o n _ 1 2 8 . p n g ' , t i t l e : ' D o n \ ' t f o r g e t ! ' , m e s s a g e : ' Y o u h a v e ' + o p e n T o d o s + ' t h i n g s t o d o . W a k e u p , d u d e ! ' 21

} , f u n c t i o n ( n o t i f i c a t i o n I d ) { } ) } } / / W h e n t h e u s e r c l i c k s o n t h e n o t i f i c a t i o n , / / w e w i l l o p e n t h e T o D o l i s t a n d d i s m i s s t h e n o t i f i c a t i o n c h r o m e . n o t i f i c a t i o n s . o n C l i c k e d . a d d L i s t e n e r ( f u n c t i o n ( n o t i f i c a t i o n I d ) { l a u n c h ( ) c h r o m e . n o t i f i c a t i o n s . c l e a r ( n o t i f i c a t i o n I d , f u n c t i o n ( ) { } ) } ) . . . changetheonAlarmlistenerto,insteadofsimplylogginganewalarmtoconsole, getstoreddataandcalltheshowNotification:

. . . c h r o m e . a l a r m s . o n A l a r m . a d d L i s t e n e r ( f u n c t i o n ( a l a r m ) { c o n s o l e . l o g ( " G o t a n a l a r m ! " , a l a r m ) c h r o m e . s t o r a g e . l o c a l . g e t ( d b N a m e , s h o w N o t i f i c a t i o n ) } ) . . .

Nowreloadyourappandspendafewminutesplayingwithit.Youshouldnoticethat: Evenwhenyouclosetheappwindow,thealarmswillkeepcoming IfyoucloseallChromewindows,alarmswon'tarrive(onplatformsotherthan ChromeOS) Whenyourappisclosed,ifyouclickonthenotificationthewindowwillopen

Gotaproblem?
Ifnotificationsarenotshowingup,checkifyourChromeversionis28.Thechrome.notifications APIwereintroducedinChrome28,soyoumightwanttochangeyourcodetousethestandard WebnotificationsAPIinstead.Wewillleavethatasachallengeforyou.TheW3Cspecifications arehere8 IfyouhaveChrome28andnotificationsstilldon'tshowup,checkerrormessagesinconsoleon boththemainwindow(rightclick>Inspectelement)andthebackgroundpage(rightclick> Inspectbackgroundpage).
8

http://www.w3.org/TR/notifications/

22

Step 4 - Parse URLs and open then in a Webview


Wanttostartfreshfromhere?Getpreviousstep'scodeins o l u t i o n _ f o r _ s t e p 3 subfolder! Objectives: learnthatitispossibletoloadandshowmostexternalcontentinsideyourappusinga webviewtaginasecureandsandboxedway Recommendedtimetocompletethisstep:10minutes Someapplicationsneedtopresentexternalwebcontentdirectlytotheuser,butkeephiminside theapplicationexperience.Forexample,anewsaggregatormightwanttoembedthenewsfrom externalsiteswithalltheformatting,imagesandbehavioroftheoriginalsite.Fortheseandother usages,ChromepackagedappshaveacustomHTMLtagcalledwebview9 .Itisaverypowerful component,butitssimplestusageisactuallyprettystraightforward,asyoumayseebelow. WewillnowchangeoursampletosearchforURLsintheToDocontentand,whenfound,adda sidelink.Thelink,whenclicked,willopenanewappwindow(notabrowsertab)withawebview presentingthecontent. 1. Inmanifest.json,requestthe"webview"permission: . . . " p e r m i s s i o n s " : [ " s t o r a g e " , " a l a r m s " , " n o t i f i c a t i o n s " , " w e b v i e w " ] , . . . 2. Createanewfile,webview.htmlwithasimple<webview>tag: < ! D O C T Y P E h t m l > < h t m l > < h e a d > < m e t a c h a r s e t = " u t f 8 " > < / h e a d > < b o d y > < w e b v i e w s t y l e = " w i d t h : 1 0 0 % h e i g h t : 1 0 0 % " > < / w e b v i e w > < / b o d y > < / h t m l > 3. Incontroller.js: addamethodtoparseURLsfromtheToDocontent.WheneveraURLisfound, ananchorreplacesit: C o n t r o l l e r . p r o t o t y p e . _ p a r s e F o r U R L s = f u n c t i o n ( t e x t ) { v a r r e = / ( h t t p s ? : \ / \ / [ ^ \ s " < > , ] + ) / g r e t u r n t e x t . r e p l a c e ( r e , ' < a h r e f = " $ 1 " d a t a s r c = " $ 1 " > $ 1 < / a > ' ) }
9

http://developer.chrome.com/trunk/apps/webview_tag.html

23

addamethodtoopenanewwindowwithwebview.htmlandsetaURLin webview.src:

C o n t r o l l e r . p r o t o t y p e . _ d o S h o w U r l = f u n c t i o n ( e ) { / / o n l y a p p l i e s t o e l e m e n t s w i t h d a t a s r c a t t r i b u t e s i f ( ! e . t a r g e t . h a s A t t r i b u t e ( ' d a t a s r c ' ) ) { r e t u r n } e . p r e v e n t D e f a u l t ( ) v a r u r l = e . t a r g e t . g e t A t t r i b u t e ( ' d a t a s r c ' ) c h r o m e . a p p . w i n d o w . c r e a t e ( ' w e b v i e w . h t m l ' , { h i d d e n : t r u e } , / / o n l y s h o w w i n d o w w h e n w e b v i e w i s c o n f i g u r e d f u n c t i o n ( a p p W i n ) { a p p W i n . c o n t e n t W i n d o w . a d d E v e n t L i s t e n e r ( ' D O M C o n t e n t L o a d e d ' , f u n c t i o n ( e ) { / / w h e n w i n d o w i s l o a d e d , s e t w e b v i e w s o u r c e v a r w e b v i e w = a p p W i n . c o n t e n t W i n d o w . d o c u m e n t . q u e r y S e l e c t o r ( ' w e b v i e w ' ) w e b v i e w . s r c = u r l / / n o w w e c a n s h o w i t : a p p W i n . s h o w ( ) } ) } ) } Parseforlinkswheneveritemsareshown:

/ * * * A n e v e n t t o f i r e o n l o a d . W i l l g e t a l l i t e m s a n d d i s p l a y t h e m i n t h e * t o d o l i s t * / C o n t r o l l e r . p r o t o t y p e . s h o w A l l = f u n c t i o n ( ) { t h i s . m o d e l . r e a d ( f u n c t i o n ( d a t a ) { t h i s . $ t o d o L i s t . i n n e r H T M L = t h i s . _ p a r s e F o r U R L s ( t h i s . v i e w . s h o w ( d a t a ) ) } . b i n d ( t h i s ) ) } / * * * R e n d e r s a l l a c t i v e t a s k s * / C o n t r o l l e r . p r o t o t y p e . s h o w A c t i v e = f u n c t i o n ( ) { t h i s . m o d e l . r e a d ( { c o m p l e t e d : 0 } , f u n c t i o n ( d a t a ) { t h i s . $ t o d o L i s t . i n n e r H T M L = t h i s . _ p a r s e F o r U R L s ( t h i s . v i e w . s h o w ( d a t a ) ) 24

} . b i n d ( t h i s ) ) } / * * * R e n d e r s a l l c o m p l e t e d t a s k s * / C o n t r o l l e r . p r o t o t y p e . s h o w C o m p l e t e d = f u n c t i o n ( ) { t h i s . m o d e l . r e a d ( { c o m p l e t e d : 1 } , f u n c t i o n ( d a t a ) { t h i s . $ t o d o L i s t . i n n e r H T M L = t h i s . _ p a r s e F o r U R L s ( t h i s . v i e w . s h o w ( d a t a ) ) } . b i n d ( t h i s ) ) } ParseforlinksintheeditItem.Also,fixthecodesothatitusestheinnerTextofthe inputelementinsteadofitsinnerHTML:

C o n t r o l l e r . p r o t o t y p e . e d i t I t e m = f u n c t i o n ( i d , l a b e l ) { . . . v a r o n S a v e H a n d l e r = f u n c t i o n ( ) { . . . / / I n s t e a d o f r e r e n d e r i n g t h e w h o l e v i e w j u s t u p d a t e / / t h i s p i e c e o f i t l a b e l . i n n e r H T M L = v a l u e l a b e l . i n n e r H T M L = t h i s . _ p a r s e F o r U R L s ( v a l u e ) . . . / / G e t t h e i n n e r H T M L o f t h e l a b e l i n s t e a d o f r e q u e s t i n g t h e d a t a f r o m t h e / / O R M . I f t h i s w e r e a r e a l D B t h i s w o u l d s a v e a l o t o f t i m e a n d w o u l d a v o i d / / a s p i n n e r g i f . i n p u t . v a l u e = l a b e l . i n n e r H T M L i n p u t . v a l u e = l a b e l . i n n e r T e x t . . . Finally,addaclicklistenerintheControllerconstructortocallthedoShowUrl methodwhenuserclicksinalink:

f u n c t i o n C o n t r o l l e r ( m o d e l , v i e w ) { t h i s . m o d e l = m o d e l t h i s . v i e w = v i e w t h i s . E N T E R _ K E Y = 1 3 t h i s . E S C A P E _ K E Y = 2 7 t h i s . $ m a i n = $ $ ( ' # m a i n ' ) 25

t h i s . $ t o g g l e A l l = $ $ ( ' # t o g g l e a l l ' ) t h i s . $ t o d o L i s t = $ $ ( ' # t o d o l i s t ' ) t h i s . $ t o d o I t e m C o u n t e r = $ $ ( ' # t o d o c o u n t ' ) t h i s . $ c l e a r C o m p l e t e d = $ $ ( ' # c l e a r c o m p l e t e d ' ) t h i s . $ f o o t e r = $ $ ( ' # f o o t e r ' ) t h i s . r o u t e r = n e w R o u t e r ( ) t h i s . r o u t e r . i n i t ( ) t h i s . $ t o d o L i s t . a d d E v e n t L i s t e n e r ( ' c l i c k ' , t h i s . _ d o S h o w U r l ) . . . Now,ifyoureloadyourapp,youshouldhavesomethinglike:

Andafterclickingonthelink:

26

Note:awebviewisasandboxedprocess.YoucanonlyinteractwithitusingitsAPI.The embedderapp(yourapp)cannoteasilyhavedirectaccesstothewebviewDOM,forexample. Advanced:Ifyouareaheadoftime,checkthewebviewdocumentationandplayalittlebitwith itsmethods,showingasmallstatusmessageorindicatorwhilethewebviewisloading.

27

Step 5 - Add images from the web


Wanttostartfreshfromhere?Getpreviousstep'scodeins o l u t i o n _ f o r _ s t e p 4 subfolder! Objectives: learnhowtoloadresourcesfromoutsideyourappandaddthemtotheDOMthrough XHRandObjectURLs Recommendedtimetocompletethisstep:20minutes ChromepackagedappsplatformforceyourapptobefullycompliantwithContentSecurity Policies.ThisincludesnotbeingabletodirectlyloadDOMresources,likeimages,fontsand CSS'sfromoutsideofyourapp.Ifyouwanttoshowanexternalimageinyourapp,youneedto requestitviaXHR,transformitintoaBlobandcreateanObjectURL.ThisObjectURLcanthen beaddedtotheDOM,becauseitreferstoaninmemoryiteminthecontextoftheapp. Let'schangeourapptolookforimageURLsintheToDocontent.IftheURLlookslikeanimage (endswith.png,.jpg,.svgor.gif),wewilldownloadtheimageandshowitonthesideofthe anchorasathumbnail. 1. Inmanifest.json,requestthe"<all_urls>"permission.InaChromepackagedappyoucan makeXMLHttpRequestcallstoanyURL,aslongasyouwhitelistitsdomaininthe manifest.Insteadoflistingspecificdomains,wewillaskpermissiontomakerequeststo "<all_urls>"becausewedon'tknowbeforehandwhatimageURLtheuserofourappwill typeintheToDocontent: . . . " p e r m i s s i o n s " : [ " s t o r a g e " , " a l a r m s " , " n o t i f i c a t i o n s " , " w e b v i e w " , " < a l l _ u r l s > " ] , . . . 2. Injs/controller.js: a. AddamethodtocreateObjectURLsfromaBlob.ObjectURLsholdmemoryand youneedtorevokewhenyounolongerneedthem,soaddalsoaclearmethod: C o n t r o l l e r . p r o t o t y p e . _ c l e a r O b j e c t U R L = f u n c t i o n ( ) { i f ( t h i s . o b j e c t U R L s ) { t h i s . o b j e c t U R L s . f o r E a c h ( f u n c t i o n ( o b j U R L ) { U R L . r e v o k e O b j e c t U R L ( o b j U R L ) } ) t h i s . o b j e c t U R L s = n u l l } } C o n t r o l l e r . p r o t o t y p e . _ c r e a t e O b j e c t U R L = f u n c t i o n ( b l o b ) { v a r o b j U R L = U R L . c r e a t e O b j e c t U R L ( b l o b ) t h i s . o b j e c t U R L s = t h i s . o b j e c t U R L s | | [ ] 28

t h i s . o b j e c t U R L s . p u s h ( o b j U R L ) r e t u r n o b j U R L } b. AddamethodtoexecuteaXMLHttpRequestonaURL,createanObjectURL fromtheXHR'sresponseandaddan<img>elementwiththisObjectURLtothe DOM: C o n t r o l l e r . p r o t o t y p e . _ r e q u e s t R e m o t e I m a g e A n d A p p e n d = f u n c t i o n ( i m a g e U r l , e l e m e n t ) { v a r x h r = n e w X M L H t t p R e q u e s t ( ) x h r . o p e n ( ' G E T ' , i m a g e U r l ) x h r . r e s p o n s e T y p e = ' b l o b ' x h r . o n l o a d = f u n c t i o n ( ) { v a r i m g = d o c u m e n t . c r e a t e E l e m e n t ( ' i m g ' ) i m g . s e t A t t r i b u t e ( ' d a t a s r c ' , i m a g e U r l ) i m g . c l a s s N a m e = ' i c o n ' v a r o b j U R L = t h i s . _ c r e a t e O b j e c t U R L ( x h r . r e s p o n s e ) i m g . s e t A t t r i b u t e ( ' s r c ' , o b j U R L ) e l e m e n t . a p p e n d C h i l d ( i m g ) } . b i n d ( t h i s ) x h r . s e n d ( ) } c. Nowaddamethodthatfindsalllinksnotyetprocessedandcheckthem.For eachURLthatlookslikeanimage,execute_requestRemoteImageAndAppend: C o n t r o l l e r . p r o t o t y p e . _ p a r s e F o r I m a g e U R L s = f u n c t i o n ( ) { / / r e m o v e o l d b l o b s t o a v o i d m e m o r y l e a k : t h i s . _ c l e a r O b j e c t U R L ( ) v a r l i n k s = t h i s . $ t o d o L i s t . q u e r y S e l e c t o r A l l ( ' a [ d a t a s r c ] : n o t ( . t h u m b n a i l ) ' ) v a r r e = / \ . ( p n g | j p g | j p e g | s v g | g i f ) $ / f o r ( v a r i = 0 i < l i n k s . l e n g t h i + + ) { v a r u r l = l i n k s [ i ] . g e t A t t r i b u t e ( ' d a t a s r c ' ) i f ( r e . t e s t ( u r l ) ) { l i n k s [ i ] . c l a s s L i s t . a d d ( ' t h u m b n a i l ' ) t h i s . _ r e q u e s t R e m o t e I m a g e A n d A p p e n d ( u r l , l i n k s [ i ] ) } } } d. AndcallitappropriatelyinshowAll,showActive,showCompletedandeditItem: C o n t r o l l e r . p r o t o t y p e . s h o w A l l = f u n c t i o n ( ) { t h i s . m o d e l . r e a d ( f u n c t i o n ( d a t a ) { 29

t h i s . $ t o d o L i s t . i n n e r H T M L = t h i s . _ p a r s e F o r U R L s ( t h i s . v i e w . s h o w ( d a t a ) ) t h i s . _ p a r s e F o r I m a g e U R L s ( ) } . b i n d ( t h i s ) ) } / * * * R e n d e r s a l l a c t i v e t a s k s * / C o n t r o l l e r . p r o t o t y p e . s h o w A c t i v e = f u n c t i o n ( ) { t h i s . m o d e l . r e a d ( { c o m p l e t e d : 0 } , f u n c t i o n ( d a t a ) { t h i s . $ t o d o L i s t . i n n e r H T M L = t h i s . _ p a r s e F o r U R L s ( t h i s . v i e w . s h o w ( d a t a ) ) t h i s . _ p a r s e F o r I m a g e U R L s ( ) } . b i n d ( t h i s ) ) } / * * * R e n d e r s a l l c o m p l e t e d t a s k s * / C o n t r o l l e r . p r o t o t y p e . s h o w C o m p l e t e d = f u n c t i o n ( ) { t h i s . m o d e l . r e a d ( { c o m p l e t e d : 1 } , f u n c t i o n ( d a t a ) { t h i s . $ t o d o L i s t . i n n e r H T M L = t h i s . _ p a r s e F o r U R L s ( t h i s . v i e w . s h o w ( d a t a ) ) t h i s . _ p a r s e F o r I m a g e U R L s ( ) } . b i n d ( t h i s ) ) } . . . C o n t r o l l e r . p r o t o t y p e . e d i t I t e m = f u n c t i o n ( i d , l a b e l ) { v a r l i = l a b e l / / T h i s f i n d s t h e < l a b e l > ' s p a r e n t < l i > w h i l e ( l i . n o d e N a m e ! = = ' L I ' ) { l i = l i . p a r e n t N o d e } v a r o n S a v e H a n d l e r = f u n c t i o n ( ) { v a r v a l u e = i n p u t . v a l u e . t r i m ( ) v a r d i s c a r d i n g = i n p u t . d a t a s e t . d i s c a r d i f ( v a l u e . l e n g t h & & ! d i s c a r d i n g ) { t h i s . m o d e l . u p d a t e ( i d , { t i t l e : i n p u t . v a l u e } ) / / I n s t e a d o f r e r e n d e r i n g t h e w h o l e v i e w j u s t u p d a t e / / t h i s p i e c e o f i t l a b e l . i n n e r H T M L = t h i s . _ p a r s e F o r U R L s ( v a l u e ) t h i s . _ p a r s e F o r I m a g e U R L s ( ) 30

. . . 3. Finally,inbower_components/todomvccommon/base.css,addaCSSruletolimitthe sizeoftheimage: . t h u m b n a i l i m g [ d a t a s r c ] { m a x w i d t h : 1 0 0 p x m a x h e i g h t : 2 8 p x } Nowreloadyourapp,gotoGoogleImageSearch,findsomeimageURLsandaddtheminToDo contents.Someexamples: http://goo.gl/nqHMF#.jpg http://goo.gl/HPBGR#.png Andthisishowitshouldlook:

Tip:Forrealworldsituations,whenyouneedtocontrolofflinecacheanddozensof simultaneousresourcedownloads,wehavecreatedahelperlibrary10 tohandlesomecommon usecases.

10

https://github.com/GoogleChrome/appsresourceloader#readme

31

Step 6 - Export ToDo's to the filesystem


Wanttostartfreshfromhere?Getpreviousstep'scodeins o l u t i o n _ f o r _ s t e p 5 subfolder! Objectives: Learnhowtogetareferencetoafileintheexternalfilesystemandwritetothisfileduring thelifetimeoftheappusingtheFileSystemAPI Recommendedtimetocompletethisstep:20minutes Inthisstep,wewilladdanexportbuttontotheapp.Whenclicked,thecurrentToDoitemswill besavedtoatextfileselectedbytheuser.Ifthefileexists,itwillbereplaced.Otherwise,anew filewillbecreated.Observethattheuseronlyneedstoselectthefileonceduringthelifetimeof theobjectrepresentingthefileentry.Inourexample,wetiedittotheappwindowsoaslongas theuserkeepsthewindowopen,theJavaScriptcodecanwritetotheselectedfilewithoutany userinteraction. 1. Inmanifest.json,requestthe{ f i l e S y s t e m : [ " w r i t e " ] } permission.Notice thatthesyntaxofthispermissionismorecomplexthantheothers,aswewillwanttonot onlyaccesstheexternalfileSystem,butalsowritetoit: . . . " p e r m i s s i o n s " : [ " s t o r a g e " , " a l a r m s " , " n o t i f i c a t i o n s " , " w e b v i e w " , " < a l l _ u r l s > " , { " f i l e S y s t e m " : [ " w r i t e " ] } ] , . . . 2. Inindex.html,addan"Exporttodisk"buttonandadivwherewewillshowastatus message.Also,loadthescriptwewillcreatenext: . . . < f o o t e r i d = " i n f o " > < b u t t o n i d = " t o g g l e A l a r m " > A c t i v a t e a l a r m < / b u t t o n > < b u t t o n i d = " e x p o r t T o D i s k " > E x p o r t t o d i s k < / b u t t o n > < d i v i d = " s t a t u s " > < / d i v > < p > D o u b l e c l i c k t o e d i t a t o d o < / p > < p > C r e a t e d b y < a h r e f = " h t t p : / / t w i t t e r . c o m / o s c a r g o d s o n " > O s c a r G o d s o n < / a > < / p > < / f o o t e r > < s c r i p t s r c = " b o w e r _ c o m p o n e n t s / t o d o m v c c o m m o n / b a s e . j s " > < / s c r i p t > < s c r i p t s r c = " b o w e r _ c o m p o n e n t s / d i r e c t o r / b u i l d / d i r e c t o r . j s " > < / s c r i p t > < s c r i p t s r c = " j s / b o o t s t r a p . j s " > < / s c r i p t > < s c r i p t s r c = " j s / h e l p e r s . j s " > < / s c r i p t > < s c r i p t s r c = " j s / s t o r e . j s " > < / s c r i p t > < s c r i p t s r c = " j s / m o d e l . j s " > < / s c r i p t > < s c r i p t s r c = " j s / v i e w . j s " > < / s c r i p t > < s c r i p t s r c = " j s / c o n t r o l l e r . j s " > < / s c r i p t > < s c r i p t s r c = " j s / a p p . j s " > < / s c r i p t > 32

< s c r i p t s r c = " j s / a l a r m s . j s " > < / s c r i p t > < s c r i p t s r c = " j s / e x p o r t . j s " > < / s c r i p t > < / b o d y > < / h t m l >

3. CreateanewJavaScript,js/export.js,withthefollowingcontent: amethodgetTodosAsTextthatreadsToDosfromchrome.storage.localand generateatextualrepresentationofthem amethodexportToFileEntrythat,givenaFileEntry,willsavetheToDo'sastextto thatfile amethoddoExportToDiskthatexecutestheexportToFileEntrymethodadded aboveifwealreadyhaveasavedFileEntryorasktheusertochooseoneinstead aclicklisteneronthe"Exporttodisk"button ( f u n c t i o n ( ) { v a r d b N a m e = ' t o d o s v a n i l l a j s ' v a r s a v e d F i l e E n t r y , f i l e D i s p l a y P a t h f u n c t i o n g e t T o d o s A s T e x t ( c a l l b a c k ) { c h r o m e . s t o r a g e . l o c a l . g e t ( d b N a m e , f u n c t i o n ( s t o r e d D a t a ) { v a r t e x t = ' ' i f ( s t o r e d D a t a [ d b N a m e ] . t o d o s ) { s t o r e d D a t a [ d b N a m e ] . t o d o s . f o r E a c h ( f u n c t i o n ( t o d o ) { t e x t + = ' ' i f ( t o d o . c o m p l e t e d ) { t e x t + = ' [ D O N E ] ' } t e x t + = t o d o . t i t l e t e x t + = ' \ n ' } , ' ' ) } c a l l b a c k ( t e x t ) } . b i n d ( t h i s ) ) } / / G i v e n a F i l e E n t r y , f u n c t i o n e x p o r t T o F i l e E n t r y ( f i l e E n t r y ) { s a v e d F i l e E n t r y = f i l e E n t r y v a r s t a t u s = d o c u m e n t . g e t E l e m e n t B y I d ( ' s t a t u s ' )

33

/ / U s e t h i s t o g e t a p r e t t y n a m e a p p r o p r i a t e f o r d i s p l a y i n g c h r o m e . f i l e S y s t e m . g e t D i s p l a y P a t h ( f i l e E n t r y , f u n c t i o n ( p a t h ) { f i l e D i s p l a y P a t h = p a t h s t a t u s . i n n e r T e x t = ' E x p o r t i n g t o ' + p a t h } ) g e t T o d o s A s T e x t ( f u n c t i o n ( c o n t e n t s ) { f i l e E n t r y . c r e a t e W r i t e r ( f u n c t i o n ( f i l e W r i t e r ) { f i l e W r i t e r . o n w r i t e e n d = f u n c t i o n ( e ) { s t a t u s . i n n e r T e x t = ' E x p o r t t o ' + f i l e D i s p l a y P a t h + ' c o m p l e t e d ' } f i l e W r i t e r . o n e r r o r = f u n c t i o n ( e ) { s t a t u s . i n n e r T e x t = ' E x p o r t f a i l e d : ' + e . t o S t r i n g ( ) } v a r b l o b = n e w B l o b ( [ c o n t e n t s ] ) f i l e W r i t e r . w r i t e ( b l o b ) / / Y o u n e e d t o e x p l i c i t l y s e t t h e f i l e s i z e t o t r u n c a t e / / a n y c o n t e n t t h a t m i g h t w a s t h e r e b e f o r e f i l e W r i t e r . t r u n c a t e ( b l o b . s i z e ) } ) } ) } f u n c t i o n d o E x p o r t T o D i s k ( ) { i f ( s a v e d F i l e E n t r y ) { e x p o r t T o F i l e E n t r y ( s a v e d F i l e E n t r y ) } e l s e { c h r o m e . f i l e S y s t e m . c h o o s e E n t r y ( { t y p e : ' s a v e F i l e ' , s u g g e s t e d N a m e : ' t o d o s . t x t ' , a c c e p t s : [ { d e s c r i p t i o n : ' T e x t f i l e s ( * . t x t ) ' , e x t e n s i o n s : [ ' t x t ' ] } ] , a c c e p t s A l l T y p e s : t r u e } , e x p o r t T o F i l e E n t r y ) } } 34

d o c u m e n t . g e t E l e m e n t B y I d ( ' e x p o r t T o D i s k ' ) . a d d E v e n t L i s t e n e r ( ' c l i c k ' , d o E x p o r t T o D i s k ) } ) ( )

Advanced:FileEntriescannotbepersistedindefinitely,whichmeansyourappwillneedtoask theusertochooseafileeverytimetheappislaunched.However,thereisanoption11 torestore aFileEntryifyourappwasforcedtorestart(runtimecrash,appupdateorruntimeupdatefor example).Ifyouareaheadoftime,trytoplaywithitbysavingtheIDreturnedbyretainEntryand restoringitonapprestart(tip:addalistenertotheonRestartedeventinthebackgroundpage)

CONGRATS!Ifyoudiditall,youshouldhaveacompleteToDoMVCpackagedappliketheone below:

11

http://developer.chrome.com/trunk/apps/fileSystem.html#methodrestoreEntry

35

36

Advanced bonus challenge: Add voice commands


Wanttostartfreshfromhere?Getpreviousstep'scodeins o l u t i o n _ f o r _ s t e p 6 subfolder! Ifyougotsofarandstillhavetime,youmightwanttotrythisveryadvancedchallenge:what aboutbeingabletoaddToDosbysayingthemtoyourapp?UsetheHTML5WebSpeechAPI12 andfollowthehighlevelstepsbelow: requestpermissionfor"audioCapture"inthemanifest.json startaudiorecognitionwhentheuserpressesanactivationbutton getanytextafteran"addnote"commandandbeforean"endnote"commandandsave asaToDoitem Thereisnocheatcodeforthischallenge,sogoandhackyourself!

12

http://www.google.com/intl/en/chrome/demos/speech.html

37

You might also like